【模板·II】树链剖分

学长给我讲树链剖分,然而我并没有听懂,还是自学有用……另外感谢一篇Blog +by 自为风月马前卒+


一、算法简述

树链剖分可以将一棵普通的多叉树转为线段树计算,不但可以实现对一棵子树的操作,还可以实现对两点之间路径的操作,或是求 LCA(看起来很高级)。

其实树链剖分不算什么特别高难的算法,它建立在 LCA、线段树、DFS序 的基础上(如果不了解这些算法的还是先把这些算法学懂再看树链剖分吧 QwQ)。又因为树链剖分的基础算法不难,树链剖分的题也逐渐被引入 OI 赛中。

不得不说,它的代码很长……尽管很多都是模板,但是还是必须理解清楚每一个模板的含义,不然容易混淆。

二、原理

(1)基本定义

重儿子:对于每一个非叶子节点u的儿子vi,若以vi为根的子树的节点数是u的所有儿子的子树的节点数中最大的,则 vi 是u的重儿子;

重边:对于每一个非叶子节点u,它与其重儿子的连边为重边;

重链:树中只包含重边的一条链,这里我们把单个元素也看成重链;

轻儿子:非重儿子的节点;轻边:非重边;

举个例子:

(2)简单定理

  • 重链的起点一定是轻点;
  • 一条轻边一定连接重链上的一点和另一条重链的起点(重链的起点算入重链);
  • 任何节点都只属于一条重链;
  • 除了叶子节点,其他所有点一定有重儿子;

(3)计算重儿子

  siz[u]: 以u节点为根的子树的节点数量(包含u);

  dep[u]:u的深度(根节点深度为1);

  fa[u]:节点u的父亲,一般来说根节点父亲为0;

  heavy[u]:u的重儿子,没有为0;

  val[u]:u的点权;

(以上是我自己定义的名字,可能不太规范)

可以直接用一个DFS从根节点开始,用递归的方式求出每一棵子树的节点数,并找到有最大节点数的儿子,作为heavy[],不要在意那些节点数相同的情况,如果出现,则只选取其中一个作为heavy[]就可以了。

具体如何实现?我还是奉上代码吧 @( ◕ x ◕ )@:

 void DFS1(int u,int Fa,int Dep)
{
dep[u]=Dep;fa[u]=Fa; //更新深度、父节点
siz[u]=; //将u本身计入siz
int MAX_siz=; //最大子树大小
for(int i=;i<lnk[u].size();i++)
if(lnk[u][i]!=Fa) //避免重复错误
{
int v=lnk[u][i];
DFS1(v,u,Dep+);
siz[u]+=siz[v]; //统计大小
if(siz[v]>MAX_siz)
MAX_siz=siz[v],heavy[u]=v; //求重儿子
}
}

(4)计算DFN序以及重链

上一步的DFS1为求重链打下了基础。

新添几个定义: ID[u]:u的DFS序(或者可以叫dfn);Top[u]:u所在的重链的起始点(即深度最小的点);fval[x]:DFS序为x的点的点权;

这次DFS,我们需要改变搜索顺序,不能按输入顺序遍历点——先搜索重儿子,再搜索其它儿子,如果没有重儿子,说明是叶子节点,结束搜索后回溯。这样我们保证了对于每一个非叶子节点u和它的重儿子v,ID[u]和ID[v]是连续的,按照这个规律,我们可以发现,在同一条重链中,相邻元素的ID总是连续的。这是一个非常重要的性质,正因为有这个性质,我们可以应用线段树来计算(等会解释)。

举个例子吧:

如何求 Top[u] 呢?

我们可以通过下传参数实现——下传一个参数 topf ,表示当前节点属于的重链起始于 topf。我们优先访问u的重儿子v,即使优先访问u所在的重链,此时u、v仍然属于同一个重链,因此topf不变,可以下传。而当我们搜索u的其他儿子v时,相当于经过了一条轻边,因此我们就到达了另一条重链的起点("简单定理"中第2条),所以将v作为topf继续下传。

 void DFS2(int u,int topf)
{
ID[u]=++ID_cnt;fval[ID_cnt]=val[u]; //ID_cnt 就是当前的DFS序;同时给fval赋值
Top[u]=topf;
if(!heavy[u]) return; //叶子节点
DFS2(heavy[u],topf);
for(int i=;i<lnk[u].size();i++)
{
int v=lnk[u][i];
if(ID[v]) continue; //避免访问重复
DFS2(v,v); //v是另一条重链的起点
}
}

(5)线段树

这才是重头戏……

线段树最明显的优点就是区间修改、区间查询,但是这一切的前提就是它修改、查询的是一个连续的区间!这就是为什么要让一条重链上的ID成为连续的一串。在这棵线段树上,区间是ID值,即我们储存的是连续一段ID值所包含的信息。

但是这仅仅是第一步……谁也不知道线段树的强大功能到底还有哪些……

◆ 修改、查询一棵子树

一个最简单的性质 —— 以u为根的子树中 DFS 序(ID)是连续的长度为siz[u]的序列。当我们知道 ID[u] 时,我们可以马上知道这棵子树中ID最大的节点ID为 (ID[u]+siz[u]-1),又因为这是一个连续区间,线段树就可以发挥它的用途了!

以u为根的一棵子树在线段树上对应区间为 [ID[u],ID[u]+siz[u]-1]。

◆ 修改、查询从u到v的一条路径(当然也是唯一路径)

我们先分三种情况(在下面的描述中,dep[u]≤dep[v]):

① u、v本来就在一条重链上

由于重链是连续一段,且 dep[u]≤dep[v] ,所以 u->v 是连续的区间 [ID[u],ID[v]] ,这样用线段树求解比较容易吧。

② u、v分别属于的重链之间隔了一条轻边

还记得之前的“简单定理”吗?既然两条重链隔了一条轻边,则该轻边一定连接了一条轻边的顶点 Top。不妨设 dep[Top[u]]≤dep[Top[v]] ,那么我们可以得到 fa[Top[v]] 属于 u 所在重链。就像求LCA一样,我们把v上移到 fa[Top[v]] ,又归属于情况①,而 v 上移的一段也是一条重链,这段重链所属区间为 [ID[Top[v]],ID[v]] (ID[Top[v]] ≤ ID[v],因为Top[v]的深度一定小于等于v)。

这样答案就变成了两个区间 [ID[Top[v]],ID[v]] 和 [ID[fa[Top[v]]],ID[u]] 。

③ 普通情况

过程越发的像LCA了——我们的目的越发清晰——将u、v移动到同一条重链上。

我们不断的重复将点 x 做下列操作:

移动到 Top[x] -> 线段树区间处理 -> 移动到 fa[x] -> 移动到 Top[x]....

给一个伪代码把:

while(u,v不在同一个重链)

{

  保证 Top[u]的深度>Top[v]的深度 //我们固定移动u,所以一定要让u移动后不会离v的重链越来越远

  记录答案/修改区间 [ID[Top[u]],ID[u]]

  u 移动到 fa[Top[u]]

}

//现在u、v在同一个重链了

转换为问题①

如何判断u、v是否属于同一个重链?还记得 Top吗? 同一个重链上的点的 Top 一定相同啊,所以只需要判断 Top 是否相同就行了。 QwQ

举个例子模拟一下:

三、一道板板题 +洛谷 3384 树链剖分模板题+

就是两种问题——对子树以及对路径。大家可以看看代码,先熟悉一下……

 /*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN=int(1e5);
int n,t,rt,mod;
vector<int> lnk[MAXN+];
int val[MAXN+];
int dep[MAXN+],fa[MAXN+],heavy[MAXN+],siz[MAXN+];
void DFS1(int u,int Fa,int Dep)
{
dep[u]=Dep;fa[u]=Fa; //更新深度、父节点
siz[u]=; //将u本身计入siz
int MAX_siz=; //最大子树大小
for(int i=;i<lnk[u].size();i++)
if(lnk[u][i]!=Fa) //避免重复错误
{
int v=lnk[u][i];
DFS1(v,u,Dep+);
siz[u]+=siz[v]; //统计大小
if(siz[v]>MAX_siz)
MAX_siz=siz[v],heavy[u]=v; //求重儿子
}
}
int ID[MAXN+],fval[MAXN+],Top[MAXN+],ID_cnt;
void DFS2(int u,int topf)
{
ID[u]=++ID_cnt;fval[ID_cnt]=val[u]; //ID_cnt 就是当前的DFS序;同时给fval赋值
Top[u]=topf;
if(!heavy[u]) return; //叶子节点
DFS2(heavy[u],topf);
for(int i=;i<lnk[u].size();i++)
{
int v=lnk[u][i];
if(ID[v]) continue; //避免访问重复
DFS2(v,v); //v是另一条重链的起点
}
}
struct TREE{
int l,r,val,siz,lazy;
TREE(){}
TREE(int fl,int fr){
l=fl,r=fr;siz=r-l+,lazy=;
}
}tree[MAXN*+];
//上传更新总和
void Update(int x){tree[x].val=((tree[x<<].val+tree[x<<|].val)%mod+mod)%mod;}
//下传懒标记
void PushDown(int x)
{
tree[x<<].val=(tree[x<<].val+tree[x<<].siz*tree[x].lazy)%mod;
tree[x<<|].val=(tree[x<<|].val+tree[x<<|].siz*tree[x].lazy)%mod;
tree[x<<].lazy=(tree[x<<].lazy+tree[x].lazy)%mod;
tree[x<<|].lazy=(tree[x<<|].lazy+tree[x].lazy)%mod;
tree[x].lazy=;
}
//构造线段树
void Build(int l,int r,int x)
{
tree[x]=TREE(l,r);
if(l==r) {tree[x].val=fval[l];return;}
int mid=(l+r)>>;
Build(l,mid,x<<);
Build(mid+,r,x<<|);
Update(x);
}
//给[L,R]的每个元素加上add
void Add(int L,int R,int add,int x)
{
if(L>tree[x].r || R<tree[x].l) return;
if(L<=tree[x].l && tree[x].r<=R)
{
tree[x].val+=tree[x].siz*add;
tree[x].lazy+=add;
return;
}
PushDown(x);
Add(L,R,add,x<<);
Add(L,R,add,x<<|);
Update(x);
}
//求[L,R]中元素的和
int Sum(int x,int L,int R)
{
if(L>tree[x].r || R<tree[x].l)
return ;
if(L<=tree[x].l && tree[x].r<=R)
return tree[x].val;
PushDown(x);
return (Sum(x<<,L,R)+Sum(x<<|,L,R))%mod;
}
//求路径 u->v 的总和
int Road(int u,int v)
{
int ret=;
while(Top[u]!=Top[v])
{
if(dep[Top[u]]<dep[Top[v]]) swap(u,v); //保证Top[u]在Top[v]下
ret=(ret+Sum(,ID[Top[u]],ID[u]))%mod; //更新答案
u=fa[Top[u]]; //移动u
}//此时u,v应该属于同一条重链了
if(dep[u]>dep[v]) swap(u,v);
ret=(ret+Sum(,ID[u],ID[v]))%mod; //同一重链中计算
return ret;
}
//修改路径 u->v
void Insert(int u,int v,int add)
{
while(Top[u]!=Top[v])
{
if(dep[Top[u]]<dep[Top[v]]) swap(u,v);
Add(ID[Top[u]],ID[u],add,); //利用线段树修改一段重链
u=fa[Top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
Add(ID[u],ID[v],add,); //最后一段u->v
}
int main()
{
scanf("%d%d%d%d",&n,&t,&rt,&mod);
for(int i=;i<=n;i++)
scanf("%d",&val[i]);
for(int i=;i<n;i++)
{
int u,v;scanf("%d%d",&u,&v);
lnk[u].push_back(v);lnk[v].push_back(u);
}
DFS1(rt,,);
DFS2(rt,rt);
Build(,n,);
while(t--)
{
int cmd;scanf("%d",&cmd);
int x,y,z;
switch(cmd)
{
case : scanf("%d%d%d",&x,&y,&z);Insert(x,y,z%mod);break;
case : scanf("%d%d",&x,&y);printf("%d\n",Road(x,y));break;
case : scanf("%d%d",&x,&y);Add(ID[x],ID[x]+siz[x]-,y%mod,);break;
case : scanf("%d",&x);printf("%d\n",Sum(,ID[x],ID[x]+siz[x]-));break;
}
}
return ;
}
其他的题目:

(补充:某dalao嫌我写的太少了 Orz)

四、奇怪的区间操作 +BZOJ 2243 染色+

还记得最初写线段树的时候,有一类题是修改区间颜色,然后查询一个区间中颜色段的数量。这就涉及到如果两个相邻区间的边缘颜色相同,则它们的边缘颜色将合并为一个,这个时候就需要把总区间的颜色段个数减一(比如区间[l,r],若[l,mid]的颜色段数量为a,[mid+1,r]的颜色段数量为b,但是mid颜色和mid+1颜色相同,则[l,r]颜色段数量为 a+b-1)。现在又在树链剖分有缘重逢……

现在查询的不是一个区间了,而是一个路径!按照之前的思路,我们可以把路径拆成多个重链,也就是多个区间,这就产生了一个问题——如何判断相邻区间的左右端点颜色是否相同?

于是……一个美妙的想法从我的脑间闪过——改变函数返回值!

之前的线段树查询都只是返回一个int的答案,但这次我返回了答案以及答案对应区间的左右端点颜色(用struct打包一下),这样我就可以判断左右端点颜色是否一样了!!(o^^o)♪

最后注意一下当查询的路径的两个端点已经移动到同一条重链上时,要判断该重链的两端点是否与之前的颜色相同,及时删减答案!

源代码

 /*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN=int(1e5);
int n,m;
int col[MAXN+],dep[MAXN+],fa[MAXN+],siz[MAXN+],hvy[MAXN+],ID[MAXN+],top[MAXN+],typ[MAXN+];
//col 原节点颜色,dep 深度,fa 父亲,siz 子树大小,hvy[u] u的重儿子,ID[u] u的编号(DFS序,用于线段树),top[u] u所在重链的起点,typ[x] ID为x的节点的颜色
vector<int> lnk[MAXN+];
void DFS1(int u,int _fa,int _dep)
{
fa[u]=_fa;dep[u]=_dep;siz[u]=; //更新父亲,深度,子树大小初始化为1(因为根节点也算入大小)
for(int i=;i<lnk[u].size();i++)
{
int v=lnk[u][i];
if(v==_fa) continue;
DFS1(v,u,_dep+);
siz[u]+=siz[v];
if(siz[hvy[u]]<siz[v]) //求得重儿子
hvy[u]=v;
}
}
int ID_cnt;
void DFS2(int u,int _top)
{
ID[u]=++ID_cnt;top[u]=_top;typ[ID[u]]=col[u];
if(!hvy[u]) return; //没有根节点的节点是叶子节点
DFS2(hvy[u],_top); //同一条重链
for(int i=;i<lnk[u].size();i++)
{
int v=lnk[u][i];
if(ID[v]) continue;
DFS2(v,v); //另一条重链的起点
}
}
struct TREE{
int l,r,lc,rc,tot,col;
//左右端点,左右端点颜色,颜色段总数,区间颜色(不为纯色值为-1)
}tree[MAXN*+];
void Update(int u) //上传参数
{
tree[u].lc=tree[u<<].lc;
tree[u].rc=tree[u<<|].rc;
tree[u].tot=tree[u<<].tot+tree[u<<|].tot-(int)(tree[u<<].rc==tree[u<<|].lc);
if(tree[u<<].col==tree[u<<|].col && tree[u<<].col!=-)
tree[u].col=tree[u<<].col;
else
tree[u].col=-;
}
void Build(int L,int R,int u) //构建线段树
{
tree[u].l=L;tree[u].r=R;
if(L==R) {tree[u].lc=tree[u].rc=tree[u].col=typ[L];tree[u].tot=;return;}
int mid=(L+R)>>;
Build(L,mid,u<<);Build(mid+,R,u<<|);
Update(u);
}
void PushDown(int u) //下传懒标记
{
if(tree[u].col==-) return;
tree[u<<].lc=tree[u<<].rc=tree[u<<].col=tree[u].col;
tree[u<<|].lc=tree[u<<|].rc=tree[u<<|].col=tree[u].col;
tree[u<<].tot=tree[u<<|].tot=;
tree[u].col=-;
}
void Modify(int L,int R,int val,int u) //修改区间[L,R]为val
{
if(tree[u].r<L || R<tree[u].l) return;
if(L<=tree[u].l && tree[u].r<=R)
{
tree[u].lc=tree[u].rc=tree[u].col=val;
tree[u].tot=;
return;
}
PushDown(u);
Modify(L,R,val,u<<);
Modify(L,R,val,u<<|);
Update(u);
}
void ModifyRoad(int u,int v,int val) //修改路径u->v为val
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
Modify(ID[top[u]],ID[u],val,);
u=fa[top[u]];
}
if(dep[u]<dep[v]) swap(u,v);
Modify(ID[v],ID[u],val,);
}
struct RETURN{
int lc,rc,tot; //左右端点颜色,颜色段数量
RETURN(){}
RETURN(int _lc,int _rc,int _tot)
{
lc=_lc;rc=_rc;tot=_tot;
}
};
RETURN Query(int L,int R,int u) //查询区间[L,R]颜色段数量
{
if(tree[u].r<L || R<tree[u].l) return RETURN(-,-,);
if(L<=tree[u].l && tree[u].r<=R)
return RETURN(tree[u].lc,tree[u].rc,tree[u].tot);
PushDown(u); //*************************************必须有这一步
RETURN resl=Query(L,R,u<<),
resr=Query(L,R,u<<|);
RETURN ret;
ret.tot=resl.tot+resr.tot;
if(resl.rc==resr.lc) ret.tot--; //合并
if(!resl.tot) ret.lc=resr.lc,ret.rc=resr.rc;
else if(!resr.tot) ret.lc=resl.lc,ret.rc=resl.rc;
else ret.lc=resl.lc,ret.rc=resr.rc;
Update(u);
return ret;
}
int QueryRoad(int u,int v) //查询路径u->v
{
int cu=-,cv=-,ret=;
while(top[u]!=top[v])
{
if(dep[top[u]]>dep[top[v]])
{
RETURN res=Query(ID[top[u]],ID[u],);
ret+=res.tot;
if(res.rc==cu) ret--;
cu=res.lc;
u=fa[top[u]];
}
else
{
RETURN res=Query(ID[top[v]],ID[v],);
ret+=res.tot;
if(res.rc==cv) ret--;
cv=res.lc;
v=fa[top[v]];
}
}
if(dep[u]>dep[v])
{
RETURN res=Query(ID[v],ID[u],);
ret+=res.tot;
if(res.lc==cv) ret--; //判断两端点
if(res.rc==cu) ret--;
}
else
{
RETURN res=Query(ID[u],ID[v],);
ret+=res.tot;
if(res.lc==cu) ret--;
if(res.rc==cv) ret--;
}
return ret;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=;i<=n;i++)
scanf("%d",&col[i]),col[i]++; //由于输入颜色有0,强制转换为正整数
siz[]=-; //重儿子hvy数组的初始值是0,而为0时,siz[0]<0,就可以转换到其他的节点!
for(int i=,u,v;i<n;i++)
scanf("%d%d",&u,&v),
lnk[u].push_back(v),
lnk[v].push_back(u);
DFS1(,,);
DFS2(,);
Build(,n,);
while(m--)
{
int u,v,x;char cmd[]="";
scanf("%s%d%d",cmd,&u,&v);
if(cmd[]=='C') scanf("%d",&x),x++,ModifyRoad(u,v,x);
else printf("%d\n",QueryRoad(u,v));
}
return ;
}

The End

Thanks for reading!

- Lucky_Glass

(Tab:如果我有没讲清楚的地方可以直接在邮箱lucky_glass@foxmail.com email我,在周末我会尽量解答并完善博客~)

【模板时间】◆模板·II◆ 树链剖分的更多相关文章

  1. [BZOJ2402]陶陶的难题II(树链剖分+线段树维护凸包+分数规划)

    陶陶的难题II 时间限制:40s      空间限制:128MB 题目描述 输入格式 第一行包含一个正整数N,表示树中结点的个数. 第二行包含N个正实数,第i个数表示xi (1<=xi<= ...

  2. BZOJ 2402 陶陶的难题II (树链剖分、线段树、凸包、分数规划)

    毒瘤,毒瘤,毒瘤-- \(30000\)这个数据范围,看上去就是要搞事的啊... 题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=2402 ...

  3. [NOI2015] 软件包管理器【树链剖分+线段树区间覆盖】

    Online Judge:Luogu-P2146 Label:树链剖分,线段树区间覆盖 题目大意 \(n\)个软件包(编号0~n-1),他们之间的依赖关系用一棵含\(n-1\)条边的树来描述.一共两种 ...

  4. bzoj1036 count 树链剖分或LCT

    这道题很久以前用树链剖分写的,最近在学LCT ,就用LCT再写了一遍,也有一些收获. 因为这道题点权可以是负数,所以在update时就要注意一下,因为平时我的0节点表示空,它的点权为0,这样可以处理点 ...

  5. bzoj1036 [ZJOI2008]树的统计Count 树链剖分模板题

    [ZJOI2008]树的统计Count Description 一棵树上有n个节点,编号分别为1到n,每个节点都有一个权值w.我们将以下面的形式来要求你对这棵树完成 一些操作: I. CHANGE u ...

  6. BZOJ 1036 树的统计Count 树链剖分模板题

    题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=1036 题目大意: 一棵树上有n个节点,编号分别为1到n,每个节点都有一个权值w.我们将 ...

  7. 树链剖分 - Luogu 3384【模板】树链剖分

    [模板]树链剖分 题目描述 已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z 操 ...

  8. 算法复习——树链剖分模板(bzoj1036)

    题目: 题目背景 ZJOI2008 DAY1 T4 题目描述 一棵树上有 n 个节点,编号分别为 1 到 n ,每个节点都有一个权值 w .我们将以下面的形式来要求你对这棵树完成一些操作:I.CHAN ...

  9. P3384 【模板】树链剖分

    P3384 [模板]树链剖分 题目描述 如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节 ...

随机推荐

  1. ETL模型设计

    传统的关系数据库一般采用二维数表的形式来表示数据,一个维是行,另一个维是列,行和列的交叉处就是数据元素.关系数据的基础是关系数据库模型,通过标准的SQL语言来加以实现. 数据仓库是多维数据库,它扩展了 ...

  2. HDU 1281——棋盘游戏——————【最大匹配、枚举删点、邻接表方式】

     棋盘游戏 Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u Submit Status ...

  3. ASP.NET操作DataTable各种方法

    转:http://www.cnblogs.com/isking/p/6178268.html http://www.cnblogs.com/sntetwt/p/3496477.html public ...

  4. 关于EasyUI datagrid 表头居中 数据列内容居右 或者居左

    cell.css("text-align",(col.halign||col.align||"")); 这里有个属性挺眼熟 : col.align 前面还有一个 ...

  5. Hibernate课程 初探一对多映射3-2 单向多对一的配置

    1 多方实体类中加入,一方类和getset方法 //多方定义一个一方的引用 private Grade grade; public Grade getGrade() { return grade; } ...

  6. 非关系型数据库(NOSQL)-Redis

    整理一波Redis 简介,与memcached比较 官网:http://redis.io Redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括 ...

  7. 从零开始的全栈工程师——js篇(正则表达式)

    正则 就是一条规则 用来检验字符串的格式 目标就是字符串 只要是通过表单提交的数据 都是字符串1.正则定义var reg = new RegExp( )var reg = /格式/ <--简写 ...

  8. Promise对象(异步编程)

    Promise对象解决函数的异步调用(跟回调函数一样) 三种状态: 未完成(pending)已完成(fulfilled)失败(rejected) 通过then函数来链式调用 目前市面上流行的一些类库:

  9. jQueryMobile(二)

    三].按钮 <!-- 一个jQueryMobile页面 --> <div data-role='page'> <div data-role='header'>< ...

  10. 关于如何等待一个元素的出现而不用一些笨拙粗暴的time.sleep()方法

    我相信这是一个非常大众化的需求,我们需要等待某一个元素的出现以此来让我们的脚本进入到下一个Step,这个等待方法最好能够设置超时时间,然后找到后迅速callback.我们也很幸运!如果你仔细看Sele ...