“父承子业。”
简单来说就是在一棵树上进行 $dp$ 操作。一般是从子节点转移到父节点。初始化为叶节点。
其可扩展性十分高。所以没有固定模板而言。但一般实现方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void dp(int x,int last) { dp[x]=; for(int e=Head[x],v;e;e=Edge[e].next) { if((v=Edge[e].to)==last) continue; dp(v,x);
}
} int main() { dp(Rt,-1); }
|
基本操作
求树的直径
两种方法
- 两遍 $bfs$ 或 $dfs$ 。第一次从任意点开始,找到该点能到达的最远距离,第二次从找到的点出发,再次找最远点。这两点就是树的直径。复杂度 $O(2n)$ 。
- 考虑树型 $dp$ 。时间复杂度 $O(n)$ 。
第一种方法不必多述,而对于第二种方法:我们用 $dp_{k,x},k \in \{0,1\},x \in n$ 来记录该点所能到达其子树的最远距离和次远距离。
例题
对于一个点 $x$ ,与它相距不超过 $k$ 的点有两种情况:
- 在 $x$ 的儿子之中。
- 在 $x$ 之上(或其父节点的另一棵子树)
那么我们进行两次遍历,第一次查找每一个点向下查找能找到的点权值,记为 dp[0][x][k]
,第二次查找父节点满足条件的点权值,将两次计算相加即可。需要进行容斥。
AC Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| #include<iostream> #include<cstdio> #include<cmath> #include<cstring> #include<cstdlib> #include<algorithm> #include<ctime> #include<iomanip> #include<queue> #include<stack> #include<map> #include<vector> #define gh() getchar() #define re register typedef long long ll; using namespace std; const int MAXN=1e5+1; const int MAXK=21; template<class T> inline void read(T &x) { x=0; char ch=gh(),t=0; while(ch<'0'||ch>'9') t|=ch=='-',ch=gh(); while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh(); if(t) x=-x; } int N,K,Val[MAXN]; struct Tree { int next,to; }Edge[MAXN<<1]; int Head[MAXN],Total; int dp[2][MAXN][MAXK]; inline void addEdge(int u,int v) { Edge[++Total]=(Tree){Head[u],v};Head[u]=Total; Edge[++Total]=(Tree){Head[v],u};Head[v]=Total; } void dfs(int x,int last) { for(int i=0;i<=K;++i) dp[0][x][i]=Val[x]; for(int e=Head[x],v;e;e=Edge[e].next) { if((v=Edge[e].to)==last) continue; dfs(v,x); for(int i=1;i<=K;++i) dp[0][x][i]+=dp[0][v][i-1]; } } void dfsSec(int x,int last) { for(int e=Head[x],v;e;e=Edge[e].next) { if((v=Edge[e].to)==last) continue; dp[1][v][1]+=dp[0][x][0]; for(int i=2;i<=K;++i) dp[1][v][i]+=dp[1][x][i-1]-dp[0][v][i-2]; dfsSec(v,x); } } int main() { read(N),read(K); for(re int i=1;i<N;++i) { int u,v; read(u),read(v); addEdge(u,v); } for(re int i=1;i<=N;++i) read(Val[i]); dfs(1,-1); for(int i=1;i<=N;++i) for(int j=0;j<=K;++j) dp[1][i][j]=dp[0][i][j]; dfsSec(1,-1); for(int i=1;i<=N;++i) printf("%d\n",dp[1][i][K]); return 0; }
|
这道题其实并不是一道纯粹树型 $dp$ 。直接用 $Tarjan$ 求所有深度的 $lca$ 即可。复杂度 $O(n+q)$ 。据说 $O(n \log n)$ 会被卡。不清楚。
AC Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| #include<iostream> #include<cstdio> #include<cmath> #include<cstring> #include<cstdlib> #include<algorithm> #include<ctime> #include<iomanip> #include<queue> #include<stack> #include<map> #include<vector> #define gh() getchar() #define re register typedef long long ll; using namespace std; const int MAXN=5e6+1; template<class T> inline void read(T &x) { x=0; char ch=gh(),t=0; while(ch<'0'||ch>'9') t|=ch=='-',ch=gh(); while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh(); if(t) x=-x; } int N,M,K[MAXN],Q; struct Tree { int next,to; }Edge[MAXN<<1]; int Head[MAXN],Total; int f[MAXN],dep[MAXN]; inline void addEdge(int u,int v) { Edge[++Total]=(Tree){Head[u],v};Head[u]=Total; } int find(int x) { if(x==f[x]) return x; return f[x]=find(f[x]); } void dpTree(int x,int last) { dep[x]=dep[last]+1; for(int e=Head[x];e;e=Edge[e].next) dpTree(Edge[e].to,x),f[Edge[e].to]=x; K[dep[x]]=K[dep[x]]?find(K[dep[x]]):x; } int main() { read(N),read(M); for(int i=1,fa;i<=N;++i) { read(fa);f[i]=i; addEdge(fa,i); } dpTree(1,-1); while(M--) { read(Q); printf("%d\n",K[Q]); } return 0; }
|
这题我还比较喜欢。不看标签我是绝对不会想到用树型 $dp$ 来解的。我们记数 $i$ 的约数和为 $f(i)$ 。而当 $f(i) \leq i$ 时,可以互相转换。如果将每一个数都看作一个节点,则我们连一条边 $c(i,f(i))$ 。然后对于这一个无向图。求出其直径即可。而对于线性求约数和。读者可以自行思考如何实现复杂度 $O(n \log n)$,或者看代码。
AC Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| #include<iostream> #include<cstdio> #include<cmath> #include<cstring> #include<cstdlib> #include<algorithm> #include<ctime> #include<iomanip> #include<queue> #include<stack> #include<map> #include<vector> #define gh() getchar() #define re register typedef long long ll; using namespace std; const int MAXN=1e6+1; template<class T> inline void read(T &x) { x=0; char ch=gh(),t=0; while(ch<'0'||ch>'9') t|=ch=='-',ch=gh(); while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh(); if(t) x=-x; } int Limit,f[MAXN],dp[3][MAXN],ans; inline void init() { for(int i=1;i<MAXN;++i) f[i]=1; for(int i=2;i<MAXN;++i) for(int j=2;i*j<MAXN;++j) f[i*j]+=i; } struct Tree { int next,to; }Edge[MAXN<<1]; int Head[MAXN],Total; inline void addEdge(int u,int v) { Edge[++Total]=(Tree){Head[u],v};Head[u]=Total; Edge[++Total]=(Tree){Head[v],u};Head[v]=Total; } void dpTree(int x,int last) { for(int e=Head[x];e;e=Edge[e].next) { int v=Edge[e].to; if(v==last||v==x) continue; dpTree(v,x); if(dp[1][x]<dp[1][v]+1) { dp[2][x]=dp[1][x]; dp[1][x]=dp[1][v]+1; } else if(dp[2][x]<dp[1][v]+1) dp[2][x]=dp[1][v]+1; ans=max(ans,dp[1][x]+dp[2][x]); } } int main() { init(); read(Limit); for(int i=1;i<=Limit;++i) if(i>=f[i]) addEdge(i,f[i]); dpTree(1,-1); printf("%d",ans); return 0; }
|
二次扫描与换根法
因为考到了所以又回来重新学习。
这个思路用于求解一棵无根树,且不定根时的最值答案。主要就是首先随机一个点为根,跑一遍 $\text{dp}$ ,再利用求得的解再一次动态规划,第二次的动态规划称为换根。
例题
求得一个根使 $\displaystyle\sum dep_i$ 最大。
不难看出,遍历所得答案的时间复杂度是 $\mathcal O(n^2)$ 。而换根 $\text{dp}$ 可以优化到 $\mathcal O(n)$ 。
设当前根节点为 $u$ ,我们接下来会将根切换到 $u$ 的子结点 $v$ 上。那么,对于这次换根的影响,我们会发现,所有 $v$ 子树上的结点(包括 $v$ ),深度都会减一,而其他结点的深度加一。
那么,就可以得到一个递推式,设以 $u$ 为根结点的深度和为 $f[u]$ ,则有:
$f[v]=f[u]-size[v]+(size[1]-size[v])$
第一次扫描时设的根结点是 $1$ 。
然后最后扫描一遍并记录最大答案即可。
AC Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| #include<bits/stdc++.h> const int MAXN=1e6+10; typedef long long ll; template<class T> inline void read(T &x) { x=0; char ch=getchar(),t=0; while(ch<'0'||ch>'9') t|=ch=='-',ch=getchar(); while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar(); if(t) x=-x; } template<class T,class ...T1> inline void read(T &x,T1 &...x1) { read(x),read(x1...); } int N; struct Tre { int next,to; }Edge[MAXN<<1]; int Head[MAXN],Total; ll dp[MAXN],Siz[MAXN],Dep[MAXN]; inline void addEdge(int u,int v) { Edge[++Total]=(Tre){Head[u],v};Head[u]=Total; Edge[++Total]=(Tre){Head[v],u};Head[v]=Total; } void dfsPre(int x,int last) { Siz[x]=1,Dep[x]=Dep[last]+1; for(int e=Head[x],v;e;e=Edge[e].next) { if((v=Edge[e].to)==last) continue; dfsPre(v,x); Siz[x]+=Siz[v]; } } void dfsCalc(int x,int last) { for(int e=Head[x],v;e;e=Edge[e].next) { if((v=Edge[e].to)==last) continue; dp[v]=dp[x]+Siz[1]-Siz[v]*2; dfsCalc(v,x); } } ll Ans=-0x3f3f3f3f,idx; int main() { read(N); for(int i=2,u,v;i<=N;++i) { read(u,v);addEdge(u,v); } dfsPre(1,0);dfsCalc(1,0); for(int i=1;i<=N;++i) if(dp[i]>Ans) { Ans=dp[i]; idx=i; } printf("%lld",idx); }
|
比较基础,甚至 $\text{H}_2\text{O}$ 了一道蓝题。
转移方程:$dp[v]=dp[x]+(Siz[1]-2Siz[v])\times val[e]$
其中 $e$ 为边 $(u,v)$ ,$Siz[u]$ 表示以 $u$ 为根结点的子树中的牛的个数。
AC Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| #include<bits/stdc++.h> #define gh() getchar() const int MAXN=1e5+10; typedef long long ll; template<class T> inline void read(T &x) { x=0; char ch=gh(),t=0; while(ch<'0'||ch>'9') t|=ch=='-',ch=gh(); while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh(); if(t) x=-x; } template<class T,class ...T1> inline void read(T &x,T1 &...x1) { read(x),read(x1...); } template<class T> inline void write(T x) { if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); } int N,C[MAXN]; struct Tre { int next,to,val; }Edge[MAXN<<1]; int Head[MAXN],Total; inline void addEdge(int u,int v,int w) { Edge[++Total]=(Tre){Head[u],v,w};Head[u]=Total; Edge[++Total]=(Tre){Head[v],u,w};Head[v]=Total; } ll dp[MAXN],Siz[MAXN],Dist[MAXN]; void dfsPre(int x,int last) { Siz[x]=C[x]; for(int e=Head[x],v;e;e=Edge[e].next) { if((v=Edge[e].to)==last) continue; Dist[v]=Dist[x]+Edge[e].val; dfsPre(v,x); Siz[x]+=Siz[v]; } } void dfsCalc(int x,int last) { for(int e=Head[x],v;e;e=Edge[e].next) { if((v=Edge[e].to)==last) continue; dp[v]=dp[x]+(Siz[1]-2*Siz[v])*Edge[e].val; dfsCalc(v,x); } } ll ans=1e18; int main() { read(N); for(int i=1;i<=N;++i) read(C[i]); for(int i=2,u,v;i<=N;++i) { ll w; read(u,v,w);addEdge(u,v,w); } Dist[1]=0; dfsPre(1,0); for(int i=1;i<=N;++i) dp[1]+=Dist[i]*C[i]; dfsCalc(1,0); for(int i=1;i<=N;++i) ans=std::min(ans,dp[i]); write(ans); }
|