求用两个人从根开始走完整个树所用的最短路程.
情况分为两种:
1.两个人从根分开走并不再会和;
2.让一个人走完其它子树,并两个人会和在未走的子树的根,得到一个子问题.
要用到一个结论:
从一个点遍历整棵树走的最小路径=树边长和*2-树直径.
#include<stdio.h> #include<string.h> #include<vector> #include<algorithm> #define min(a,b) (a)<(b)?(a):(b) #define max(a,b) (a)>(b)?(a):(b) #define maxn 100010 using namespace std; const int inf=500000001; typedef struct nd { int v; int d; }nd; vector<nd>e[maxn]; int n,s; int mxl[maxn]; int td[maxn]; int dp[maxn]; void input() { int i; int a,b,c; nd tmp; for(i=1;i<n;i++) { scanf("%d%d%d",&a,&b,&c); tmp.v=b;tmp.d=c; e[a].push_back(tmp); tmp.v=a;tmp.d=c; e[b].push_back(tmp); } } void init() { int i; for(i=1;i<=n;i++) e[i].clear(); memset(mxl,-1,sizeof(mxl)); memset(td,-1,sizeof(td)); memset(dp,-1,sizeof(dp)); } int init_td(int fa,int cur) { int i,res=0; nd ck; for(i=0;i<e[cur].size();i++) if(e[cur][i].v!=fa) { ck=e[cur][i]; res+=init_td(cur,e[cur][i].v)+e[cur][i].d; } return td[cur]=res; } int init_mxl(int fa,int cur) { int i,res=0; for(i=0;i<e[cur].size();i++) if(e[cur][i].v!=fa) { int tmp=init_mxl(cur,e[cur][i].v); res=max(res,tmp+e[cur][i].d); } return mxl[cur]=res; } bool cmp(const nd& a, const nd& b){ return mxl[a.v]+a.d > mxl[b.v]+b.d; } int dfs(int fa,int cur) { int i,res,cut=0; nd ck; res=td[cur]*2-mxl[cur]; for(i=0;i<e[cur].size();i++) if(e[cur][i].v != fa) { ck=e[cur][i]; int t=td[cur]-td[e[cur][i].v]-e[cur][i].d; int tmp=dfs(cur,e[cur][i].v)+t*2+e[cur][i].d*2; res=min(res,tmp); } for(i=0;i<e[cur].size();i++) if(e[cur][i].v != fa) { cut+=mxl[e[cur][i].v]+e[cur][i].d; break; } for(i++;i<e[cur].size();i++) if(e[cur][i].v != fa) { cut+=mxl[e[cur][i].v]+e[cur][i].d; break; } res=min(td[cur]*2-cut,res); return dp[cur]=res; } void solv() { int i; init_td(0,s); init_mxl(0,s); for(i=1;i<=n;i++) sort(e[i].begin(),e[i].end(),cmp); dfs(0,s); printf("%d\n",dp[s]); } int main() { while(scanf("%d%d",&n,&s)!=EOF) { init(); input(); solv(); } return 0; }
给一个二叉树,每个点有它的权值,剪去一些边保留q条边,求最大能保留多少权值.(要保证剪完后仍然是一颗树)
这是个简单的问题:
dp[i][j]为以i为根,保留j条边的最大权值.
dp[i][j]=max{dp[ls][t]+dp[rs][j-2-t]}
但是非常具有启发性:
如果将二叉树换为普通树,将得到问题的拓展版.
假设普通树有k个孩子,可以将问题看做容量为q,物品数为k*q的01背包
1 void dfs(int fa,int cur) 2 { 3 int i,j,k; 4 for( i=0; i<e[cur].size();i++) 5 if(e[cur][i] != fa){ 6 int v=e[cur][i]; 7 dfs(cur,v); 8 for(j=q;j>0;j--) 9 for(k=0;k<j;k++) 10 dp[cur][j]=max(dp[cur][j],dp[cur][j-k-1]+dp[v][k]); 11 } 12 } 13 /* 14 注意更新dp[][]的两层循环不能颠倒成 15 for(k=0;k<q;k++) 16 for(j=q;j>0;j--) 17 if(j-k>=0) 18 dp[cur][j]=max(dp[cur][j],dp[cur][j-k-1]+dp[v][k]); 19 因为dp【cur][j-k-1]更新dp[cur][j]的时候,可能已经被dp[v][]更新过了, 20 于是dp[v][]被加过两次. 21 */
因为遍历树是有顺序的,遍历到u的儿子vn时v1~vn-1都已经用来更新过dp[u][]而vn及其以后的儿子都没有用来更新,
所以这样dp是清楚的,不会产生混乱和重复.
给一个带权的普通树,求走k步能获得的最大权值.
将dp分为两部分:
dp[0][i][j]从i点出发走j步并回来获得的最大权值
dp[1][i][j]从i点出发走j步不回来能获得的最大权值.
然后用类似 Binary Apple Tree 的方式更新.
#include<stdio.h> #include<string.h> #include<vector> #define maxn 222 #define max(a,b) (a)>(b)?(a):(b) using namespace std; vector<int> e[maxn]; int n,m,d[2][maxn][maxn],val[maxn],ans=-1; void input() { int i,u,v; for(i=1;i<=n;i++)scanf("%d",&val[i]); for(i=1;i<n;i++){ scanf("%d%d",&u,&v); e[u].push_back(v); e[v].push_back(u); } } void DP(int fa,int cur,int maxm) { int i,j,k,v; d[0][cur][0]=val[cur],d[1][cur][0]=val[cur]; for(i=0;i<e[cur].size();i++) if(e[cur][i]!=fa){ v=e[cur][i]; DP(cur,v,maxm-1); for(j=maxm;j>=0;j--) for(k=0;k<j;k++){ if(j-2-k>=0 && d[0][cur][j-k-2]>=0 && d[0][v][k]>=0) d[0][cur][j]=max(d[0][cur][j],d[0][cur][j-2-k]+d[0][v][k]); if(j-1-k>=0 && d[1][v][k]>=0 && d[0][cur][j-1-k]>=0) d[1][cur][j]=max(d[1][cur][j],d[1][v][k]+d[0][cur][j-1-k]); if(j-2-k>=0 && d[1][cur][j-2-k]>=0 && d[0][v][k]>=0) d[1][cur][j]=max(d[1][cur][j],d[1][cur][j-2-k]+d[0][v][k]); } } } void flush() { int i; for(i=0;i<=n;i++) e[i].clear(); } int main() { //freopen("test.txt","r",stdin); while(scanf("%d%d",&n,&m)!=EOF) { input(); memset(d,-1,sizeof(d)); ans=-1; DP(0,1,m); for(int i=0;i<=m;i++)for(int j=0;j<2;j++){ ans=max(ans,d[j][1][i]); } printf("%d\n",ans); flush(); } return 0; }
给n个点的树,和m条新加的边,问有多少种方式,删掉原树的一条边和新加的一条边使图分为两部分.
每加上一条新边,都会产生环,如果原树中有一条边只在一个环中,那么删掉它和对应的新边就会使图分为两部分.
当然,还要统计出原树中未成为环的边,因为只需删掉它们中一条就可以使图分为两部分.
统计的方式十分巧妙,用dp[v]表示点v到他父亲的边,如果加入的边为e(u,v),
那么 dp[u]++, dp[v]++, dp[lca(u,v)]-=2; 最后深度遍历一遍,得到所有边的值.
其实仔细想想也不难发现它的正确性.
这题的重点是发现分类标准,和找到统计方式.
1 #include<stdio.h> 2 #include<string.h> 3 #include<vector> 4 #include<math.h> 5 #define maxn 100100 6 using namespace std; 7 struct edge{ 8 int v,nxt; 9 };edge e[maxn*2]; 10 int first[maxn*2],ne; 11 int dgr[maxn*2],idx[maxn],nd[maxn*2],sz,rmq[maxn*2][20],vis[maxn]; 12 int f[maxn],n,m; 13 14 void add_edge(int u,int v){ 15 e[ne].v=v;e[ne].nxt=first[u]; 16 first[u]=ne++; 17 } 18 void input() 19 { 20 int i,v,u; 21 ne=0; 22 memset(first,-1,sizeof(first)); 23 for(i=1;i<n;i++){ 24 scanf("%d%d",&u,&v); 25 add_edge(u,v); 26 add_edge(v,u); 27 } 28 } 29 void dfs(int cur,int deep) 30 { 31 int i; 32 dgr[++sz]=deep;nd[sz]=cur; 33 idx[cur]=sz; 34 for(i=first[cur];i!=-1;i=e[i].nxt) 35 if(!vis[e[i].v]){ 36 vis[e[i].v]=1; 37 dfs(e[i].v,deep+1); 38 dgr[++sz]=deep; nd[sz]=cur; 39 } 40 } 41 void init_rmq() 42 { 43 int i,j,lmt,lft,rgt; 44 for(i=1;i<=sz;i++)rmq[i][0]=i; 45 for(i=1;(1<<i)<=sz;i++) 46 for(j=1;j+(1<<i)-1<=sz;j++) 47 { 48 rmq[j][i]=rmq[j][i-1]; 49 lft=rmq[j][i-1]; 50 rgt=rmq[j+(1<<(i-1))][i-1]; 51 if(dgr[lft]<dgr[rgt])rmq[j][i]=lft; 52 else rmq[j][i]=rgt; 53 } 54 } 55 int LCA(int l,int r) 56 { 57 int rg,k,lft,rgt; 58 if(l>r)swap(l,r); 59 rg=r-l+1; 60 k=log((double)rg)/log(2.0); 61 lft=rmq[l][k]; 62 rgt=rmq[r-(1<<k)+1][k]; 63 if(dgr[lft]<dgr[rgt])return nd[lft]; 64 return nd[rgt]; 65 } 66 int statis(int cur) 67 { 68 int i; 69 for(i=first[cur];i!=-1;i=e[i].nxt) 70 if(!vis[e[i].v]){ 71 vis[e[i].v]=1; 72 f[cur]+=statis(e[i].v); 73 } 74 return f[cur]; 75 } 76 77 int main() 78 { 79 int u,v,lca; 80 int ans; 81 //freopen("test","r",stdin); 82 scanf("%d%d",&n,&m); 83 input(); 84 sz=0; 85 memset(vis,0,sizeof(vis));vis[1]=1; 86 dfs(1,0); 87 init_rmq(); 88 memset(f,0,sizeof(f)); 89 for(int i=0;i<m;i++){ 90 scanf("%d%d",&u,&v); 91 lca=LCA(idx[u],idx[v]); 92 f[u]++;f[v]++;f[lca]-=2; 93 } 94 memset(vis,0,sizeof(vis));vis[1]=1; 95 statis(1); 96 ans=0; 97 for(int i=2;i<=n;i++) 98 if(f[i]==0 || f[i]==1){ 99 if(f[i]==0)ans+=m; 100 if(f[i]==1)ans++; 101 } 102 printf("%d\n",ans); 103 return 0; 104 }
问题可以描述为:旅人在树的k点,要访问一系列点,求最小路程.
造树就行了,方法很多,我是用bfs不断找要访问点的父亲,也可以dfs一遍用dp[v]表示从v点下去是否有需要访问的点.
这题很恶心地卡了stl,害我wa了8次,看来以后使用stl的时候要小心.
1 #include<stdio.h> 2 #include<string.h> 3 #include<queue> 4 #include<vector> 5 #define maxn 55555 6 using namespace std; 7 struct edge{ 8 int v,nxt; 9 }; 10 edge e[maxn*2],t[maxn*2]; 11 int he[maxn*2],ht[maxn*2],ne,nt,d1[maxn*2],d2[maxn*2]; 12 13 int n,k,vis[maxn],fa[maxn],rcd[maxn],bg,len[maxn],total; 14 queue<int>q; 15 void add_edge_e(int u,int v,int dist) 16 { 17 e[ne].v=v;e[ne].nxt=he[u];d1[ne]=dist; 18 // printf("d1[%d]=%d\n",ne,d1[ne]); 19 he[u]=ne++; 20 } 21 void add_edge_t(int u,int v,int dist) 22 { 23 t[nt].v=v;t[nt].nxt=ht[u];d2[nt]=dist; 24 // printf("d2[%d]=%d\n",nt,d2[nt]); 25 ht[u]=nt++; 26 } 27 void input() 28 { 29 int i,u,v,dist; 30 memset(he,-1,sizeof(he));ne=0; 31 memset(ht,-1,sizeof(ht));nt=0; 32 for(i=1;i<n;i++){ 33 scanf("%d%d%d",&u,&v,&dist); 34 add_edge_e(u,v,dist); 35 add_edge_e(v,u,dist); 36 } 37 scanf("%d",&k); 38 for(i=1;i<=k;i++)scanf("%d",&vis[i]); 39 } 40 void dfs(int f,int cur) 41 { 42 int i; 43 fa[cur]=f; 44 for(i=he[cur];i!=-1;i=e[i].nxt) 45 if(e[i].v!=f){ 46 // printf("fa=%d cur=%d dist=%d\n",cur,e[cur][i],d1[cur][i]); 47 dfs(cur,e[i].v); 48 } 49 } 50 int find_dist(int cur,int fa_cur) 51 { 52 int i; 53 for(i=he[fa_cur];i!=-1;i=e[i].nxt){ 54 // printf("e[%d].v=%d cur=%d\n",i,e[i].v,cur); 55 if(e[i].v==cur){ 56 return d1[i]; 57 } 58 } 59 return -1; 60 } 61 void bfs() 62 { 63 int i,cur,nxt,dist; 64 memset(rcd,0,sizeof(rcd)); 65 rcd[bg]=1; 66 for(i=1;i<=k;i++){ 67 q.push(vis[i]); 68 rcd[vis[i]]=1; 69 } 70 while(!q.empty()){ 71 cur=q.front();q.pop(); 72 nxt=fa[cur]; 73 dist=find_dist(cur,nxt); 74 total+=dist; 75 // printf("fa=%d cur=%d dist=%d\n",nxt,cur,dist); 76 add_edge_t(nxt,cur,dist); 77 if(!rcd[nxt]){ 78 rcd[nxt]=1; 79 q.push(nxt); 80 } 81 } 82 } 83 void init_len(int cur) 84 { 85 int i; 86 for(i=ht[cur];i!=-1;i=t[i].nxt){ 87 init_len(t[i].v); 88 len[cur]=max(len[cur],len[t[i].v]+d2[i]); 89 } 90 } 91 int main() 92 { 93 // freopen("test","r",stdin); 94 scanf("%d%d",&n,&bg);{ 95 input(); 96 dfs(0,bg); 97 total=0; 98 bfs(); 99 memset(len,0,sizeof(len)); 100 init_len(bg); 101 printf("%d\n",total*2-len[bg]); 102 // flush(); 103 } 104 return 0; 105 }
经典的树形dp,求树中以每个点v为起点的最长路径.
对每个点v记录两个值
Len[0][v]:从v点出发的一条最长路径.
Len[1][v]:从v点出发经过其它儿子的路径中最长的一条.
然后从根开始dfs:
当前树的根为u,儿子为v,
则len[0][v]=max{
len[0][v];
len[0][u]+d[u][v];
}
如果v在len[0][u]这条路径上,那么要替换成len[1][u].
mxl[v]更新完后,更新维护len[0][v].
#include<stdio.h> #include<string.h> #define max(a,b) (a)>(b)?(a):(b) #define maxn 10011 struct edge{ int v,nxt,d; };edge e[maxn*2]; struct nd{ int d,id; };nd l[2][maxn]; int hd[maxn*2],ne,n,mxl[maxn]; void add_edge(int u,int v,int d) { e[ne].v=v;e[ne].nxt=hd[u]; e[ne].d=d;hd[u]=ne++; } void input() { int i,v,d; memset(hd,-1,sizeof(hd)); for(i=2,ne=0;i<=n;i++){ scanf("%d%d",&v,&d); add_edge(i,v,d); add_edge(v,i,d); } } void dfs1(int fa,int cur) { int i,id=0; for(i=hd[cur];i!=-1;i=e[i].nxt) if(e[i].v != fa) dfs1(cur,e[i].v); for(i=hd[cur];i!=-1;i=e[i].nxt) if(e[i].v != fa && l[0][cur].d<l[0][e[i].v].d+e[i].d) l[0][cur].d=l[0][e[i].v].d+e[i].d,id=e[i].v; l[0][cur].id=id; for(i=hd[cur];i!=-1;i=e[i].nxt) if(e[i].v!=fa && e[i].v!=id && l[1][cur].d<l[0][e[i].v].d+e[i].d) l[1][cur].d=l[0][e[i].v].d+e[i].d,l[1][cur].id=e[i].v; mxl[cur]=l[0][cur].d; } void dfs2(int fa,int cur) { int i,v,j; for(i=hd[cur];i!=-1;i=e[i].nxt) if(e[i].v!=fa){ v=e[i].v; if(l[0][cur].id == v){ if(l[0][v].d<l[1][cur].d+e[i].d){ l[0][v].d=l[1][cur].d+e[i].d,l[0][v].id=cur; } else if(l[1][v].d<l[1][cur].d+e[i].d) l[1][v].d=l[1][cur].d+e[i].d;l[1][v].id=cur; }else if(l[0][v].d<l[0][cur].d+e[i].d){ l[0][v].d=l[0][cur].d+e[i].d,l[0][v].id=cur; } dfs2(cur,e[i].v); } } int main() { int i; while(scanf("%d",&n)!=EOF){ input(); memset(l,0,sizeof(l)); dfs1(0,1); dfs2(0,1); for(i=1;i<=n;i++)printf("%d\n",l[0][i].d); } return 0; }
问题可以推展为求树的顶点v所在的最长路径.
记录下最近遇到的一些比较麻烦的树形dp统计问题:
Hourai Jeweled (2012多校1 by bupt)
题目给出一颗节点带有权值,各边有颜色的树,并定义 '合法路径' 为相邻边颜色不同的路径.
合法路径的权值就是路径中各点权值的累加,求统计所有路径的权值和.
我用一个含两域的结构题保存了每个节点的信息:
jew[u].ne: u能传递给它father的路径数.
jew[u[.val: u传递给他father的路径的权值和
我们dfs的时候要做的事就是利用儿子节点来更新根结点的jew 和 统计各种连接情况下的权值和:
1. 当前节点u是路径的末端点.
2.当前节点u是连接它某两个儿子的合法路径中的点;
3.当前点u是连接它某个儿子和它父亲的合法路径中的点.
1,2可以直接利用儿子节点的jew来统计,3可以转化为它祖先的第一种情况.
#include<stdio.h> #include<string.h> #include<vector> #include<algorithm> #define maxn 300030 using namespace std; typedef long long llong; struct nd{ int v,c; }; struct nd2{ llong val; llong ne; }; vector<nd> e[maxn]; nd2 jew[maxn]; int n,val[maxn]; llong cnt; bool cmp(const nd& a,const nd& b){ return a.c <b.c; } void input() { int i,u,v,c; nd cur; for(i=1;i<=n;i++)scanf("%d",&val[i]); for(i=1;i<n;i++){ scanf("%d%d%d",&u,&v,&c); cur.v=v,cur.c=c; e[u].push_back(cur); cur.v=u,cur.c=c; e[v].push_back(cur); } for(i=1;i<=n;i++) sort(e[i].begin(),e[i].end(),cmp); } void print(int fa,nd cur) { printf("\n**************\n"); printf("Father Edge Color:%d\n",cur.c); printf("father=%d son=%d\n",fa,cur.v); printf("Edge count=%d val=%d\n",jew[cur.v].ne,jew[cur.v].val); printf("\n**************\n"); } void dfs(int col,int fa,int cur) { int i,l=0; llong com=0,comedg=0; llong total=0,totedg=0; for(i=0;i<e[cur].size();i++) if(e[cur][i].v != fa){ dfs(e[cur][i].c,cur,e[cur][i].v); if(e[cur][i].c != col){ // print(cur,e[cur][i]); jew[cur].ne+=jew[e[cur][i].v].ne; jew[cur].val+=jew[e[cur][i].v].val; } total+=jew[e[cur][i].v].val; totedg+=jew[e[cur][i].v].ne; } cnt+=totedg*val[cur]+total; jew[cur].ne++; jew[cur].val+=jew[cur].ne*val[cur]; for(i=0;i<e[cur].size();i++) if(e[cur][i].v != fa){ if(e[cur][i].c != e[cur][l].c){ cnt+=(totedg-comedg)*com+comedg*(total-com)+comedg*(totedg-comedg)*val[cur]; total-=com; totedg-=comedg; com=0; comedg=0; l=i; } com+=jew[e[cur][i].v].val; comedg+=jew[e[cur][i].v].ne; } } void flush() { int i; for(i=1;i<=n;i++)e[i].clear(); } int main() { // freopen("data.in","r",stdin); while(scanf("%d",&n)!= EOF) { input(); memset(jew,0,sizeof(jew)); cnt=0; dfs(-1,0,1); printf("%lld\n",cnt); flush(); } return 0; }
Holiday's Accommodatio (2011 Chen'Du Regional Problem H, hdu 4118)
题目背景是来自n个城市的家庭要访问其它家庭所在的城市,求所有家庭能移动的最大距离和,题目要求不能有两个家庭移动到同一个城市,
并且移动过程中都是走的最短路.
题目的要求跟最大权匹配类似,但是数据规模巨大,直接跑匹配肯定是行不通的.
这个问题的统计方法很巧妙,考虑一条边e,设它左边有x个节点,右边有y个节点,那么它最多被通过min(x,y) * 2次,所以用树形dp统计出每个节点为根
的子树的节点数就可以了.
#include<stdio.h> #include<string.h> #define maxn 100010 #define min(a,b) (a)<(b)?(a):(b) typedef long long llong; typedef struct Edge{ int v; int nxt; llong w; }Edge;Edge e[maxn<<1]; int head[maxn<<1],n,cnte,cntp[maxn],pre[maxn],cnts; bool vis[maxn]; llong ans; void add_edge(int u,int v,llong w) { e[cnte].v=v;e[cnte].nxt=head[u]; e[cnte].w=w;head[u]=cnte++; } void input() { int i,u,v; llong w; memset(head,-1,sizeof(head));cnte=0; scanf("%d",&n); for(i=1;i<n;i++){ scanf("%d%d%I64d",&u,&v,&w); add_edge(u,v,w); add_edge(v,u,w); } } void init_cntp() { int cur=1,i; pre[cur]=0; memset(vis,0,sizeof(vis)); memset(cntp,0,sizeof(cntp)); cntp[cur]=1; vis[1]=1; while(cur) { for(i=head[cur];i!=-1;) if(!vis[e[i].v]){ vis[e[i].v]=1; pre[e[i].v]=cur; cur=e[i].v; cntp[cur]=1; i=head[cur]; }else i=e[i].nxt; cntp[pre[cur]]+=cntp[cur]; cur=pre[cur]; } } void statis() { int cur=1,i; llong a,b; ans=0; pre[cur]=0; memset(vis,0,sizeof(vis)); vis[cur]=1; while(cur) { for(i=head[cur];i!=-1;) if(!vis[e[i].v]){ vis[e[i].v]=1; a=cntp[cur]-cntp[e[i].v]; b=cntp[e[i].v]; ans+=(min(a,b)) * e[i].w*2; cntp[e[i].v]=cntp[cur]; pre[e[i].v]=cur; cur=e[i].v; i=head[cur]; } else i=e[i].nxt; cur=pre[cur]; } printf("%I64d\n",ans); } int main() { int cas,t; // freopen("test.txt","r",stdin); scanf("%d",&cas); for(t=1;t<=cas;t++){ printf("Case #%d: ",t); input(); init_cntp(); statis(); } return 0; }
hdu用dfs会爆栈,写成了非递归的形式.