这几个题练习DFS序的一些应用。
问题引入:
给定一颗n(n <= 10^5)个节点的有根树,每个节点标有权值,现有如下两种操作:
1、C x y 以节点x的权值修改为y。
2、Q x 求出以节点x为根的子树权值和。
最直观的做法, 枚举一个子树内所有节点的权值加和。但这种做法的每一次讯问的时间复杂度是O(n)的,很明显无法满足题目的需要,我们需要更优的解法。
我们考虑DFS序的另外一种形式,当访问到一个节点时记下当前的时间戳,我们设它为L[x], 结束访问一个节点时当前的时间戳设为R[x]。则以x为根的子树刚好是下标为L[x]和R[x]中间的点。我们看下面这个例子。
有了DFS序这个性质我们就可以讲这个问题转化成一个区间求和问题。
每一次询问的复杂度就是O(logn)级别的,完全可以接受。
另外:很多人习惯在生成DFS序的时候,叶子节点的进出时间戳差1,我习惯于叶子节点的进出时间戳一样。这样根节点的时间戳为[1,n]
比如说:上图中如果采用叶子节点进出时间戳差1的写法,DFS序为:4、3、1、7、7、1、2、6、6、2、3、5、8、8、5、4.数量为2*n
如果按照我习惯的做法,DFS序为:4、3、1、7、2、6、5、8.
两种做法都可以,我习惯于第二种。
练习题:
第一题:http://acm.hdu.edu.cn/showproblem.php?pid=3887
题意:给定一棵树,求某一节点所在的子树中比它的值小的节点数量。
思路:求给定节点的时间戳区间,则在此区间的DFS序就是该节点的子树中的所有节点。统计这个区间中的比给定节点值小的数目即可。
这里我们可以变换一种思维,从后往前找。因为值最大的节点之间肯定都是比它小的。求出后删除掉,就不会影响小标号的节点统计了。
区间维护用线段树或者树状数组均可。我用线段树。
为了避免卡Vector,建树用前向星模拟一下。
为了避免卡递归DFS爆栈,用手工模拟非递归DFS。
#include <iostream> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #include <vector> #include <queue> #include <stack> using namespace std; #define Maxn 100005 #define lx (x<<1) #define rx ((x<<1) | 1) #define MID ((l + r)>>1) int n,s; int total = 0; //前向星 int first[Maxn]; int next[Maxn<<1]; struct Edge { int a,b; }edge[Maxn<<1]; int S[Maxn<<2]; int vis[Maxn]; struct Node { int l,r; }node[Maxn]; int nodeNum = 0; int f[Maxn]; stack <int> st; void insert(int a,int b) { total++; edge[total].a = a; edge[total].b = b; next[total] = first[a]; first[a] = total; } //递归dfs会爆栈 /*void dfs(int s) { nodeNum++; vis[s] = 1; node[s].l = nodeNum; for(int i=first[s];i!=-1;i=next[i]) { if(!vis[edge[i].b]) dfs(edge[i].b); } node[s].r = nodeNum; }*/ void dfs(int s) { st.push(s); while(!st.empty()) { int flag = 0; int t = st.top(); if(!vis[t]) { nodeNum++; node[t].l = nodeNum; vis[t] = 1; } for(int i=first[t];i!=-1;i=next[i]) { if(!vis[edge[i].b]) { st.push(edge[i].b); flag = 1; } } //叶子节点 //把pop放在最后 if(!flag) { node[t].r = nodeNum; st.pop(); } } } //线段树相关 void pushUp(int x) { S[x] = S[lx] + S[rx]; } void build(int l,int r,int x) { if(l == r) { S[x] = 1; return; } build(l,MID,lx); build(MID+1,r,rx); pushUp(x); } void update(int p,int d,int l,int r,int x) { if(l == r) { S[x] += d; return; } if(p<=MID) update(p,d,l,MID,lx); else update(p,d,MID+1,r,rx); pushUp(x); } int query(int L,int R,int l,int r,int x) { if(L<=l && r<=R) { return S[x]; } int ans = 0; if(L<=MID) ans += query(L,R,l,MID,lx); if(MID+1<=R) ans += query(L,R,MID+1,r,rx); return ans; } void init() { total = 0; nodeNum = 0; memset(first,-1,sizeof(first)); memset(next,-1,sizeof(next)); memset(vis,0,sizeof(vis)); memset(f,0,sizeof(f)); while(!st.empty()) { st.pop(); } } int main() { #ifndef ONLINE_JUDGE freopen("in2.txt","r",stdin); #endif int a,b; while(scanf(" %d %d",&n,&s)!=EOF) { init(); if(n+s == 0) break; for(int i=0;i<n-1;i++) { scanf(" %d %d",&a,&b); insert(a,b); insert(b,a); } dfs(s); build(1,n,1); for(int i=n;i>=1;i--) { f[i] = query(node[i].l,node[i].r,1,n,1) - 1; update(node[i].l,-1,1,n,1); } for(int i=1;i<=n;i++) { i == n ? printf("%d\n",f[i]) : printf("%d ",f[i]); } } return 0; }
第二题: http://poj.org/problem?id=3321
题意是统计某一节点所在子树的节点个数。伴随着节点的删除增加操作。
#include <iostream> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #include <vector> #include <queue> using namespace std; #define Maxn 100005 #define lx (x<<1) #define rx ((x<<1) | 1) #define MID ((l + r)>>1) //卡Vector,改成前向星吧 //点邻接的第一个边号 int first[Maxn]; int next[Maxn<<1]; int edgeNum; struct Edge { int a,b; }edge[Maxn<<1]; int vis[Maxn]; int total = 0; int n,m; int S[Maxn<<2]; struct Node { int l,r; }node[Maxn]; void insert(int a,int b) { edgeNum++; edge[edgeNum].a = a; edge[edgeNum].b = b; next[edgeNum] = first[a]; first[a] = edgeNum; } void dfs(int s) { //printf("hello world\n"); total++; node[s].l = total; vis[s] = 1; for(int i = first[s];i!=-1;i = next[i]) { if(!vis[edge[i].b]) dfs(edge[i].b); } node[s].r = total; } void pushUp(int x) { S[x] = S[lx] + S[rx]; } void build(int l,int r,int x) { if(l == r) { S[x] = 1; return; } build(l,MID,lx); build(MID+1,r,rx); pushUp(x); } int query(int L,int R,int l,int r,int x) { if(L<=l && r<=R) return S[x]; int ans = 0; if(L<=MID) ans += query(L,R,l,MID,lx); if(MID+1<=R) ans += query(L,R,MID+1,r,rx); pushUp(x); return ans; } void update(int p,int d,int l,int r,int x) { if(l == r) { S[x] = d - S[x]; return; } if(p<=MID) update(p,d,l,MID,lx); else update(p,d,MID+1,r,rx); pushUp(x); } void init() { total = 0; edgeNum = 0; memset(vis,0,sizeof(vis)); memset(next,-1,sizeof(next)); memset(first,-1,sizeof(first)); } int main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); #endif int a,b,c; char op[5]; init(); scanf(" %d",&n); for(int i=0;i<n-1;i++) { scanf(" %d %d",&a,&b); insert(a,b); insert(b,a); } dfs(1); build(1,n,1); scanf(" %d",&m); for(int i=0;i<m;i++) { scanf(" %s %d",op,&c); if(op[0] == 'Q') { int ans = query(node[c].l,node[c].r,1,n,1); printf("%d\n", ans); } else if(op[0] == 'C') { update(node[c].l,1,1,n,1); } } return 0; }
第三题:http://www.lydsy.com/JudgeOnline/problem.php?id=1103
某一节点到根节点距离的维护问题。
思路:使用DFS O(n)预处理出所有节点到根的路径上的权值和,将一个点的权值增加b就相当于将这个以这个点为根的子树中所有节点到根的距离增加b,使用线段树的lazy标记。
虽然此题是区间更新,单点查询。我们也可以用区间更新,区间查询的思路。
#include <iostream> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #include <vector> #include <queue> #include <stack> #include <algorithm> using namespace std; #define Maxn 250005 #define lx (x<<1) #define rx ((x<<1) | 1) #define MID ((l + r)>>1) int n,m; int first[Maxn]; int next[Maxn]; int father[Maxn]; int vis[Maxn]; int rank[Maxn]; //本题处理成有向边,就不用开双倍内存了 struct Edge { int a,b; }edge[Maxn]; int edgeNum = 0; int totalNum = 0; struct Node { int l,r; int roadNum; }node[Maxn]; //处理成有向边吧 void insert(int a,int b) { edgeNum++; edge[edgeNum].a = a; edge[edgeNum].b = b; next[edgeNum] = first[a]; first[a] = edgeNum; } void dfs(int s,int deep) { totalNum++; node[s].l = totalNum; rank[totalNum] = s; node[s].roadNum = deep; vis[s] = 1; for(int i=first[s];i!=-1;i=next[i]) { if(!vis[edge[i].b]) dfs(edge[i].b,deep+1); } node[s].r = totalNum; } void init() { memset(first,-1,sizeof(first)); memset(next,-1,sizeof(next)); memset(father,0,sizeof(father)); memset(vis,0,sizeof(vis)); edgeNum = 0; totalNum = 0; } int S[Maxn<<2]; int D[Maxn<<2]; void pushUp(int x) { //S[x] = S[lx] + S[rx]; //我们只需要叶节点的信息,写个伪的吧 return; } void pushDown(int l,int r,int x) { if(D[x]) { D[lx] += D[x]; D[rx] += D[x]; S[lx] += (MID-l+1)*D[x]; S[rx] += (r-MID)*D[x]; D[x] = 0; } } void build(int l,int r,int x) { if(l == r) { S[x] = node[rank[l]].roadNum; return; } build(l,MID,lx); build(MID+1,r,rx); pushUp(x); } void update(int L,int R,int d,int l,int r,int x) { if(L<=l && r<=R) { D[x] += d; S[x] += (r - l + 1)*d; return; } pushDown(l,r,x); if(L<=MID) update(L,R,d,l,MID,lx); if(MID+1<=R) update(L,R,d,MID+1,r,rx); pushUp(x); } int query(int L,int R,int l,int r,int x) { if(L<=l && r<=R) { return S[x]; } int ans = 0; pushDown(l,r,x); if(L<=MID) ans += query(L,R,l,MID,lx); if(R>=MID+1) ans += query(L,R,MID+1,r,rx); return ans; } int main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); #endif init(); int a,b; int opa,opb; char op[5]; scanf(" %d",&n); for(int i=0;i<n-1;i++) { scanf(" %d %d",&a,&b); if(a>b) swap(a,b); insert(a,b); } dfs(1,0); build(1,n,1); scanf(" %d",&m); m = n + m - 1; for(int i=0;i<m;i++) { scanf(" %s",op); if(op[0] == 'W') { scanf(" %d",&opa); int ans = query(node[opa].l,node[opa].l,1,n,1); printf("%d\n", ans); } else if(op[0] == 'A') { scanf(" %d %d",&opa,&opb); if(opa>opb) swap(opa,opb); update(node[opb].l,node[opb].r,-1,1,n,1); } } return 0; }