LCA-最近公共祖先

  LCA(Least Common Ancestors),即最近公共祖先,这种描述是基于树结构的,也即我们通通常只在树结构中考虑祖先问题。树实际上就是图论中的有向无环图,而要研究LCA问题,首先我们要指定树中的一个顶点为根节点,并以该节点遍历有向无环图,生成一颗DFS序下的树,假设我们要查询的两个节点为u,v,DFS序下根节点到两点的最短路径分别是(r,u),和(r,v),LCA就是(r,u)与(r,v)公共路径的最后一个节点,如下图所示,w即为LCA。

  换句话说,u,v的LCA就是以r为根的树中,u到v的最短路径中深度最小的点(假设根节点的深度为1,而深度是往下递增的)。

暴力

  暴力法求解一对节点的LCA时时间复杂度是O(n)的,所以当查询多对节点的LCA时,暴力算法的时间复杂度往往不满足要求。
  暴力法就是通过不断地将深度较深的点往上求父节点,直到两个点的父节点重合时,即可得到LCA。下面给出一种实现方法。

int dfs1(int k,int u,int v){//暴力法1的dfs方法,u,v为要查询的两个节点 int count=0; int ans=-1;for(int i=0;i<g[k].size();i++){if(!color[g[k][i]]){color[g[k][i]]=1;int num=dfs1(g[k][i],u,v);if(num>=0){count++;ans=num;} }}//如果有两个分支返回返回的是节点编号,那么此时该节点一定是LCA,返回节点编号 if(count==2)ans=k;//遍历到u,或者v时,返回该点编号即可 if(k==u||k==v)ans=k;//否则返回-1,也即不返回任何节点编号 return ans;
}
int violence1(int u,int v){memset(color,0,sizeof(color));return dfs1(0,u,v);//假设根节点为0
}

倍增算法

  倍增法其实就是每一步尽可能跳的多一点,他的思想与二分的想法其实是一致的,假设我们要求解LCA(u,v),暴力的想法是我们始终将深度较深的往上跳跃一步,直到u,v的深度第一次相等时,此时该节点就是LCA(u,v),但是这样做的话,在一个接近线性的树中,时间复杂度是O(n)的,当有多组查询时,这种开销就无法承受了。
  倍增的原理是每一次尽可能地多跳一些步数,而不是一步一步往上跳,当时如何快速找到应该跳的步数呢?考虑到depth[LCA(u,v)]-depth[u]是一个深度差值,而一开始我们是不知道LCA(u,v)到底是哪一个节点,并且u和v也可能不在同一深度,那么为了便于计算,我们应该首先间u和v调整到同一深度。
  当u和v的深度相同时,此时depth[LCA(u,v)]-depth[u]=depth[LCA(u,v)]-depth[v],也即u和v跳相同的步数即可到达最近公共祖先。由于此时不知道LCA(u,v)到底是哪一个,所以此时跳跃的步数只能使尝试性的,否则我们可能会直接跳过头,那么什么样的尝试序列是比较好的呢?考虑到搜索算法的时间复杂度上限,我们可以选择跳跃2^j步进行尝试。
  但是这里面存在一个问题就是,当u和v跳跃2^j步后,两者的祖先相等,此时该节点就是u和v的一个公共祖先,但是却不一定是最近公共祖先,所以为了避免这种情况的出现,我们选择u和v跳跃2 ^j步后,两者的祖先不是公共祖先的最大j,进行跳跃;跳跃2 ^j步后,此时u,v依然在同一深度,与之前的子问题是等价的,我们可以继续该操作,直到j==0跳出循环;由于我们每一步都不会直接跳到最近公共祖先,所以最后得到的结果中,u和v都跳跃到了最近公共祖先的下一层,此时我们直接返回u或者v的父节点即可。
  上述跳跃2 ^j的操作是通过一个dp数组来实现的,我们用dp[i][j]表示节点i的第2 ^j个祖先,那么dp[i][j]可以由更小的子问题推导出,显然我们可以得到dp[i][j]=dp[dp[i][j-1]][j-1];这样我们在遍历到每一个节点是预处理出以该节点的所有可能的dp值即可。初始化时dp[i][0]其实就是i的父节点,我们可以直接在遍历的时候赋值。
  下图是该算法每一次尝试跳跃时所做的选择。

例题

LCA模板题

#include<bits/stdc++.h>
using namespace std;//LCA算法复习
//动态规划+倍增优化const int N=1000010;
int edge[N];
int nest[N];
int last[500010];
int cnt=1;inline void add(int u,int v){nest[cnt]=last[u];edge[cnt]=v;last[u]=cnt;cnt++;return;
}
//用来保存父节点
int dp[500010][20];
//保存深度
int depth[500010];
//预处理出父亲切点
bool vise[500010];
void DFS(int k){//预处理出DP数组for(int i=1;(1<<i)<depth[k];i++){dp[k][i]=dp[dp[k][i-1]][i-1];}for(int i=last[k];i;i=nest[i]){//求解直接公共祖先;if(vise[edge[i]])continue;vise[edge[i]]=true; depth[edge[i]]=depth[k]+1;dp[edge[i]][0]=k;DFS(edge[i]);}return;
}
int lca(int u,int v){if(depth[u]<depth[v])swap(u,v);//弹节点 int k=log2(depth[u]-depth[v]);for(int i=k;i>=0;i--){if(depth[dp[u][i]]>=depth[v])u=dp[u][i];} if(u==v)return u;//查询k=log2(depth[u]);for(int i=k;i>=0;i--){if(dp[u][i]==dp[v][i])continue;u=dp[u][i];v=dp[v][i];} return dp[u][0];
}
int main(){//LCA模板题int n,m,s;cin>>n>>m>>s;int u,v;for(int i=0;i<n-1;i++){scanf("%d %d",&u,&v);add(u,v);add(v,u);} //这里要初始化为1,避免与深度为0的0产生歧义。 depth[s]=1;vise[s]=true;DFS(s);for(int i=0;i<m;i++){scanf("%d %d",&u,&v);cout<<lca(u,v)<<endl;}return 0;}

  上述算法的预处理时间复杂度是O(nlogn),每次查询时间是O(logn)的。总的时间复杂度是O(nlon+mlogn)的;其中m为查询次数。
  注:上述算法主要有三个核心部分①中序前序遍历建立兄弟链表法的二叉树结构;②DFS获得预处理数据;③倍增法查询LCA。

ST算法

  看了一下网上的博客,解释的也不是很清楚,只是给出了一个DFS序,然后给出了实现代码,但是并没有实质上的原理阐述,而我根据我的理解对该算法进行解读吧。
  要想理解一般树的LCA算法,我觉得首先要理解二叉树的LCA算法,二叉树中由于每个节点只有两个孩子,假设我们要求解LCA的两个节点为u和v,考虑到在二叉树的中序遍历中有这样一个特性,也即u和v的LCA,一定在u和v之间遍历,而u和v的其他非LCA祖先,一定在u和v遍历次序之外,也即假设由中序遍历次序…u…v…,那么LCA(u,v)一定在u,v中间,也即有…u…LCA(u,v)…v…,而LCA(u,v)的祖先节点则一定在v之后遍历。
  这样,如果我们可以发现一个区间[u,v]中LCA(u,v)具有的特性,即可快速找到该点,考虑到区间[u,v]中(我们考虑的是中序遍历中的遍历区间);LCA(u,v)的深度是最小的,那么如果我们可以快速得到区间[u,v]中的最小值即可求解出LCA(u,v)了;
  显然对于数组区间最值查询问题我们可以借用ST算法或者线段树实现,这里不再介绍这两种算法的时间过程,感兴趣可以看我的另一篇博客;
  上述思想实际上是在二叉树里面实现的,由于二叉树每个节点最多只有两个子节点,所以我们有中序遍历的说法,但是实际上一般树中是不存在中序遍历的, 我们通常只有DFS的遍历方式,那么如何在一般树中达到上述结构描述的性质呢?我们上述算法中要查询LCA(u,v)时,默认了LCA(u,v)一定在区间[u,v]中,而通常的DFS却不能保证这个性质,为了达到上述效果,我们需要保证我们在一般树中得到的遍历序列中,父节点的任意两个孩子之间都有父节点存在;
  为了达到上述效果,我们可以冗余存储,也即每当我们遍历到当前节点的一个分支时,我们就将当前节点也添加进去,这样我们就可以保证任意两个孩子节点都有一个父节点存在了,那么我们就可以按照二叉树中的思想求解LCA算法了。

例题1

LVA模板题

#include<bits/stdc++.h>
using namespace std;const int N=5000005;
//兄弟链表法存储所有边
int edge[2*N];
int nest[2*N];
int last[N];
int cnt=1;
inline void add(int u,int v){nest[cnt]=last[u];edge[cnt]=v;last[u]=cnt;cnt++;return;
}int sq[2*N];
int d[N];
bool vise[N];
int has[N];
void DFS(int k){for(int i=last[k];i;i=nest[i]){if(vise[edge[i]])continue;vise[edge[i]]=true;d[edge[i]]=d[k]+1;DFS(edge[i]);sq[++cnt]=edge[i];has[edge[i]]=cnt;sq[++cnt]=k;}return;
}
int dp[2*N][20];int lca(int l,int r){if(l>r)swap(l,r);int k=log2(r-l+1);if(d[dp[l][k]]<d[dp[r-(1<<k)+1][k]]){return dp[l][k];}else{return dp[r-(1<<k)+1][k];}
}
int main(){int n,m,s;cin>>n>>m>>s;int u,v;for(int i=0;i<n-1;i++){scanf("%d %d",&u,&v);add(u,v);add(v,u);}cnt=0;vise[s]=true;d[s]=1;DFS(s);for(int i=0;i<=cnt;i++)dp[i][0]=sq[i];for(int j=1;j<=20;j++){for(int i=0;i+(1<<j)<=cnt+1;i++){//这里一定要考虑区间的边界,要满足超出区间右侧一位 if(d[dp[i][j-1]]<d[dp[i+(1<<j-1)][j-1]]){dp[i][j]=dp[i][j-1];}else{dp[i][j]=dp[i+(1<<j-1)][j-1];}}}has[s]=cnt;for(int i=0;i<m;i++){scanf("%d %d",&u,&v);cout<<lca(has[u],has[v])<<endl;} return 0;
}

例题2

LCA

#include<bits/stdc++.h>
using namespace std;
//LCA ST算法实现
#define N 100001
int ins[N];
int mid[N];
unordered_map<int,int> has;//构建二叉树时,用于寻找中序遍历中节点的位置
unordered_map<int,int> tran;//节点编码,编码到1到N的自然数上
unordered_map<int,int> retran;//解码,返回原始的输入数据
int edge[2*N][2];
int last[N];
int id=0;
//添加边
inline void add(int u,int v){//添加边,一定要记住  edge[id][0]=last[u];edge[id][1]=v;last[u]=id;id++;edge[id][0]=last[v];edge[id][1]=u;last[v]=id;id++;return;
}
void dfs(int l,int r,int pre,int later,int k){if(k>0)add(k,ins[l]);if(l==r)return;int index=has[ins[l]];int dis=index-pre;if(dis>=1)dfs(l+1,l+dis,pre,index-1,ins[l]);if(l+dis+1<=r)dfs(l+dis+1,r,index+1,later,ins[l]);return ;
}
//arr中存的是深度,而ans中存的是节点的坐标
int arr[2*N];
int ans[2*N];
int dep[N];
int first[N];
int cnt=0;
void DFS(int k){for(int i=last[k];i!=-1;i=edge[i][0]){if(dep[edge[i][1]]==0){ dep[edge[i][1]]=dep[k]+1;DFS(edge[i][1]); first[edge[i][1]]=cnt;arr[cnt]=dep[edge[i][1]];ans[cnt]=edge[i][1];cnt++;//每遍历一次子树,就将父节点添加一次 arr[cnt]=dep[k];ans[cnt]=k;cnt++;}}return ;
}
int main(){int m,n;cin>>m>>n;//重映射,数据中给出的节点的值,而不是序号,这里随便指定一个序号。 for(int i=0;i<n;i++)cin>>mid[i];for(int i=0;i<n;i++)cin>>ins[i];for(int i=0;i<n;i++){tran[ins[i]]=i+1;retran[i+1]=ins[i];}for(int i=0;i<n;i++)ins[i]=tran[ins[i]];for(int i=0;i<n;i++)mid[i]=tran[mid[i]];for(int i=0;i<n;i++)has[mid[i]]=i;memset(last,-1,sizeof(last));//last是一定要初始化为-1的,否则无法DFS dfs(0,n-1,0,n-1,0); //初始化第一个位置 memset(dep,0,sizeof(dep)); memset(ans,0,sizeof(ans));dep[ins[0]]=1;DFS(ins[0]);//生成一个特殊的DFS序列 //注意单独处理出根节点的first数组,由于最后一个一定是根节点,所以我们直接将first设置为cnt-1即可first[ins[0]]=cnt-1; //预处理ST算法的DP数组 int dp[cnt][(int)log2(cnt)+1];for(int i=0;i<cnt;i++)dp[i][0]=i;//保存的是下标 //C++当数组开辟空间不够时,会占用重写已经开辟的空间,造成不可预知的错误 //一定有一个最值 for(int i=1;(1<<i)<cnt;i++){for(int j=0;j+(1<<i)-1<cnt;j++){if(arr[dp[j][i-1]]<arr[dp[j+(1<<i-1)][i-1]]){dp[j][i]=dp[j][i-1];}else{dp[j][i]=dp[j+(1<<i-1)][i-1];}}}for(int i=0;i<m;i++){int u,v;cin>>u>>v;int a=tran[u];int b=tran[v];bool tag=false;if(a==0){cout<<"ERROR: "<<u;tag=true;}if(b==0){if(tag){cout<<" and "<<v<<" are not found."<<endl;continue;}else{cout<<"ERROR: "<<v<<" is not found."<<endl;continue;}} if(tag){cout<<" is not found."<<endl;continue;   }bool tip=false;if(first[a]>first[b]){swap(a,b);swap(u,v);tip=true;}int f;int k=log2(first[b]-first[a]+1);if(arr[dp[first[a]][k]]<arr[dp[first[b]-(1<<k)+1][k]]){f=ans[dp[first[a]][k]];}else{f=ans[dp[first[b]-(1<<k)+1][k]];}if(f==a){cout<<u<<" is an ancestor of "<<v<<"."<<endl;continue;        }if(f==b){cout<<v<<" is an ancestor of "<<u<<"."<<endl;continue;}if(!tip)cout<<"LCA of "<<u<<" and "<<v<<" is "<<retran[f]<<"."<<endl;else  cout<<"LCA of "<<v<<" and "<<u<<" is "<<retran[f]<<"."<<endl;}return 0;}

  上述是LCA的ST算法实现方式,主要原理便是我上面所描述的,但是实际上针对本题可以简化DFS生成序列的过程,因为二叉树本身的中序遍历已经有了,我们只需要求出每个节点的深度即可运用ST算法。

tarjan算法

  终于到了最后的tarjan算法,断断续续也写了好几天了,现在来讲一讲tarjan的原理,刚开始看这个算法时,我觉得相比于倍增和ST算法来说是更难理解的,因为没有那种迸发的灵感,上面两个想法其实一开我就有一些想法的,只是有一些细节上有所缺失,但是当了解关键点以后,还是可以很轻易理解的,而tarjan算法一开始给我的感觉就是很乱,就像是做数学题要分类讨论,而我脑子里却一团糟的感觉。
  但是仔细梳理以后还是发现了该算法的神奇之处,实际上好的讲解也可以比较轻松的理解该算法的,好了,废话不多说了,直接上干货。
  首先还是要分析一下我们图算法里面最核心的知识——遍历,我们考虑DFS后序遍历时的特点(也即先遍历孩子节点,再遍历当前节点),对于两个节点u,v来说,他们要么在最近公共祖先的两个子树中,要么就是其中一个是祖先节点,一个是子孙节点。而DFS遍历时,我们遍历到u以及v时具有什么特殊的性质呢?
  如果u是先遍历到的,此时由于不知道v的信息,所以我们也无法对最近公共祖先做出判断。但是如果u是后遍历到的, 而v是先遍历到的,那么u遍历结束后,在DFS递归的返回过程中,u一定会不断向上返回,只到某一刻返回到u和v的LCA节点,那么我们如何快速知晓这个节点到底是哪一个呢?考虑到LCA(u,v)一定在u,v的上层,而如果我们将LCA(u,v)的v所在的子树内部的LCA问题都处理结束后,我们其实可以将这个子树中点集视为一个点,因为该子树中所有节点与子树之外节点的LCA只能是LCA(u,v)本身或者LCA(u,v)的父节点了。那么当我们将这个集合中每个点的父节点都设置为LCA(u,v),在探查到u时,我们就可以通过v的父节点快速获得LCA(u,v)的值。
  对于将集合中每一个点的父节点都设置为LCA(u,v)的过程,我们可以使用并查集来实现,因为这是一个一个的子问题;当某一个节点s的所有分支的节点的可处理LCA问题都处理结束后,此时s的子树和s已经是一个并查集了,并且并查集的根节点是s,当s也处理结束后,我们可以直接将s并在其父节点的并查集中,并且将其父节点设置为根节点。这样后续节点与s以及s子树中节点的LCA问题的父节点只可能是s的祖先节点,而不可能是s或者s的子孙节点。而后续节点要想快速查询到与已经遍历过某个顶点的LCA时,直接查询已经遍历过顶点所在并查集的根节点即可。而查到的根节点一定是LCA,因为根节点总是后于要查询节点处理结束的。

例题1

LCA模板题

#include<bits/stdc++.h>
using namespace std;//tarjan离线算法实现LCA
const int N=5000005;
int arr[N];
int find(int x){return x==arr[x]?x:arr[x]=find(arr[x]);
}
//这里的merge不能用秩平衡定理
void merge(int x,int y){x=find(x);y=find(y);arr[x]=y;return;
}
//兄弟链表法存储所有边
int edge[2*N];
int nest[2*N];
int last[N];
int cnt=1;
inline void add(int u,int v){nest[cnt]=last[u];edge[cnt]=v;last[u]=cnt;cnt++;return;
}
//用来存储所有的查询以及查询的序号
vector<int> q[N];
vector<int> qi[N];
void add_query(int u,int v,int id){q[u].push_back(v);q[v].push_back(u);qi[u].push_back(id);qi[v].push_back(id);return;
}int vise[N];
int ans[N];void DFS(int k){//初次访问 vise[k]=1; for(int i=last[k];i;i=nest[i]){if(vise[edge[i]])continue;DFS(edge[i]);//注意这里只能单向合并,可以直接设置父节点 merge(edge[i],k);}//这里一定要注意遍历顺序是后根序。 //查看是否已经有可以求解的for(int i=0;i<q[k].size();i++){if(vise[q[k][i]]==2){ans[qi[k][i]]=find(q[k][i]);}}//遍历结束 vise[k]=2;return;
}
int main(){ int n,m,s;cin>>n>>m>>s;//初始化并查集for(int i=1;i<=n;i++)arr[i]=i; int u,v;for(int i=0;i<n-1;i++){scanf("%d %d",&u,&v);add(u,v);add(v,u);}for(int i=0;i<m;i++){scanf("%d %d",&u,&v);add_query(u,v,i);}DFS(s);for(int i=0;i<m;i++){cout<<ans[i]<<endl;}
}

例题2

POJ1330

#include<iostream>
#include<vector>
#include<string.h>
using namespace std;
#define N 100001
int arr[N];
int find(int x){return x==arr[x]?x:arr[x]=find(arr[x]);
}
void merge(int x,int y){arr[x]=y;return ;
}
int edge[N*2][2];
int last[N];
int id=0;
void add(int u,int v){edge[id][0]=last[u];edge[id][1]=v;last[u]=id;id++;return ;
}
int vise[N];
int ans=0;
int a=0,b=0;
void DFS(int k){for(int i=last[k];i!=-1;i=edge[i][0]){if(vise[edge[i][1]]==0){DFS(edge[i][1]);if(a==edge[i][1]&&vise[b]==1){ans=find(b);  } if(b==edge[i][1]&&vise[a]==1){ans=find(a);}vise[edge[i][1]]=1;merge(edge[i][1],k);//并查集中将父节点设置为根,便于后续查询,这里是并查集变种 //由于我们使用了路径压缩,实际上时间复杂度是较低的。   }}return;
}
int main(){std::ios::sync_with_stdio(false);int T;cin>>T;for(int i=0;i<T;i++){int n;cin>>n;int tip[N];int u,v;memset(tip,0,sizeof(tip));memset(last,-1,sizeof(last));//一定要初始化 //初始化并查集for(int i=1;i<=n;i++)arr[i]=i; for(int i=0;i<n-1;i++){cin>>u>>v;add(u,v);tip[v]=true;}cin>>a>>b;for(int i=1;i<=n;i++){if(tip[i]==0){memset(vise,0,sizeof(vise));DFS(i);break;}}cout<<ans<<endl; }return 0;
}

LCA算法以及原理详解相关推荐

  1. 文本分类算法TextCNN原理详解

    详情请看:膜拜大佬![原创]文本分类算法TextCNN原理详解(一) - ModifyBlog - 博客园 Textcnn 原理 与rnn lstm 的比较, Textcnn更快 textCNN的总结 ...

  2. 机器学习经典算法决策树原理详解(简单易懂)

    ↑ 点击上方[计算机视觉联盟]关注我们 最经典的决策树算法有ID3.C4.5.CART,其中ID3算法是最早被提出的,它可以处理离散属性样本的分类,C4.5和CART算法则可以处理更加复杂的分类问题, ...

  3. JVM垃圾回收算法与原理详解

    垃圾回收 参考文档 GC参考手册-Java版 理解Java的强引用.软引用.弱引用和虚引用 JVM系列(五) - JVM垃圾回收算法 如何判断对象可以回收 引用计数法 参考文章 Java JVM的引用 ...

  4. 目标跟踪算法KCF原理详解

    一直以来没有很想写这个,以为这个东西比较简单,还算是比较容易理解的一个算法,但是在知乎上回答过一个问题之后就有朋友私信我一些关于细节的东西,我一直以为关于细节的东西大家可以自己去理解,大家都是想快速了 ...

  5. 检索算法: BM25原理详解

    文章目录 简介 Function k1k_1k1​ 的作用 bbb 的作用 终极总结 参考文档 简介 BM25算法常用来进行搜索. 输入问题Q0Q_0Q0​,在数据中去匹配其它Q时,可以用BM25进行 ...

  6. NLP之---word2vec算法skip-gram原理详解

    https://blog.csdn.net/weixin_41843918/article/details/90312339

  7. CRF(条件随机场)与Viterbi(维特比)算法原理详解

    摘自:https://mp.weixin.qq.com/s/GXbFxlExDtjtQe-OPwfokA https://www.cnblogs.com/zhibei/p/9391014.html C ...

  8. EMD算法之Hilbert-Huang Transform原理详解和案例分析

    目录 Hilbert-Huang Transform 希尔伯特-黄变换 Section I 人物简介 Section II Hilbert-Huang的应用领域 Section III Hilbert ...

  9. 操作系统:基于页面置换算法的缓存原理详解(下)

    概述: 在上一篇<操作系统:基于页面置换算法的缓存原理详解(上)>中,我们主要阐述了FIFO.LRU和Clock页面置换算法.接着上一篇说到的,本文也有三个核心算法要讲解.分别是LFU(L ...

最新文章

  1. 算法岗面试复盘:阿里,百度,作业帮,华为
  2. 补充spring事务传播性没有考虑的几种情况
  3. python人脸识别框很小_人脸识别:从传统方法到深度学习
  4. 理论基础 —— 排序 —— 鸡尾酒排序
  5. Windows services相关命令
  6. 如何做研究-精辟分析
  7. 河北四部门联合打击虚开骗税违法行为
  8. 象棋名手手机版2019最新版_象棋名手超强版下载
  9. QXDM 存储log的方法
  10. STM8S103之AD采样
  11. 如何快速计算出三相电流的数值呢
  12. oracle中那个日期怎么相减_oracle 日期相减
  13. word怎样在方框内打勾/打叉 word方框打勾教程
  14. Echarts图表之南丁格尔图
  15. 山西等保测评机构项目测评收费价格标准参考
  16. [乡土民间故事_徐苟三传奇]第廿三回_吃蒸肉财主受捉弄
  17. java clh_AQS基础——多图详解CLH锁的原理与实现
  18. 学术期刊数据库---EBSCO
  19. 不要签名证书将网页打包封装成苹果APP,无需苹果企业签名,IPA证书签名,ios签名证书,免越狱安装...
  20. 页面直接调用打印机,打印pdf文件

热门文章

  1. 页面功能:设为首页和加入收藏
  2. 浏览器打开jsp显示源码
  3. Linux安装Charm-crypto环境详细流程
  4. Pycharm-用鼠标滚轮放大缩小字体大小
  5. 人工智能创业项目,创业服务资源渠道
  6. 静态html左侧导航菜单代码,Html+Css+Jquery实现左侧滑动拉伸导航菜单栏的示例代码...
  7. bugreport实战
  8. 视频伪流技术(pseudostreaming)
  9. 科研工具EndNote文献排序
  10. ODBC 连接操作postgresql