树形dp
高老师让我更我就更了...一道一道看吧
1.ZJU 3201 Tree of Tree
给出一棵N个点的无根树,每个节点都有对应的权值
现要求你找出一棵K个节点的子树,使得这个子树上的权值和最大
题解:dp[v][i]表示以v为根下的子树选i个点的最大权值和,做一下背包就可以了
代码源网侵删
(ZOJ密码忘了我真是...)
#include<cstdio> #include<iostream> #include<cstdlib> #include<algorithm> #include<cmath> #include<cstring> #include<vector> #include<queue> using namespace std; int head[maxn],dp[maxn][maxn],tol,num[maxn]; struct node{ int next,to; }edge[maxn]; void add(int u,int v){ edge[tol].to=v; edge[tol].next=head[u]; head[u]=tol++; } int m; void dfs(int u,int fa) { for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].to; if(v==fa)continue; dfs(v,u); for(int j=m;j>1;j--) for(int k=1;k<j;k++) dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]); } } int main() { int i,j,k,n; while(cin>>n>>m){ memset(head,-1,sizeof(head)); tol=0; memset(dp,0,sizeof(dp)); for(i=0;i<n;i++) cin>>j,dp[i][1]=j; for(i=1;i<n;i++){ scanf("%d%d",&j,&k); add(j,k); add(k,j); } dfs(0,-1); int ans=0; for(i=0;i<n;i++) ans=max(ans,dp[i][m]); cout<<ans<<endl; } return 0; }
2.POJ1155 TELE
给定一棵树,1为根结点表示电视台,有m个叶子节点表示客户,有n-m-1个中间节点表示中转站,每条树边有权值。现在要在电视台播放一场比赛,每个客户愿意花费cost[i]的钱观看,而从电视台到每个客户也都有个费用,并且经过一条边只会产生一个费用。问电视台不亏损的情况最多有几个客户可以看到比赛
题解:也是一个背包,dp[v][i]表示以v为根的子树下连i个用户最多剩多少钱,这样答案就是dp[1][i]中满足dp[1][i]>0且最大的那个i
写一下转移方程吧,家里上不去poj
for(int i=size[v];i>=0;i--) for(int j=0;j<=i;j++) if(dp[vson][j]!=-INF&&dp[v][i-j]!=-INF) dp[v][i]=max(dp[v][i],dp[v][i-j]+dp[vson][j]-w); //w为连接v和vson的费用
3.NOI2002 贪吃的九头龙
传说中的九头龙是一种特别贪吃的动物。虽然名字叫“九头龙”,但这只是说它出生的时候有九个头,而在成长的过程中,它有时会长出很多的新头,头的总数会远大于九,当然也会有旧头因衰老而自己脱落。 有一天,有M个脑袋的九头龙看到一棵长有N个果子的果树,喜出望外,恨不得一口把它全部吃掉。可是必须照顾到每个头,因此它需要把N个果子分成M组,每组至少有一个果子,让每个头吃一组。 这M个脑袋中有一个最大,称为“大头”,是众头之首,它要吃掉恰好K个果子,而且K个果子中理所当然地应该包括唯一的一个最大的果子。果子由N-1根树枝连接起来,由于果树是一个整体,因此可以从任意一个果子
出发沿着树枝“走到”任何一个其他的果子。 对于每段树枝,如果它所连接的两个果子需要由不同的头来吃掉,那么两个头会共同把树枝弄断而把果子分开;如果这两个果子是由同一个头来吃掉,那么这个头会懒得把它弄断而直接把果子连同树枝一起吃掉。
当然,吃树枝并不是很舒服的,因此每段树枝都有一个吃下去的“难受值”,而九头龙的难受值就是所有头吃掉的树枝的“难受值”之和。 九头龙希望它的“难受值”尽量小,你能帮它算算吗? 【输入】 输入的第1行包含三个整数N(1<=N<=300),M(2<=M<=N),K(1<=K<=N)。N个果子依次编号1,2,...,N,且最大的果子的编号总是1。第2行到第N行描述了果树的形态,每行包含三个整数a(1<=a<=N),
b(1<=b<=N),c(0<=c<=10^5),表示存在一段难受值为c的树枝连接果子a和果子b。 【输出】 输出仅有一行,包含一个整数,表示在满足“大头”的要求的前提下,九头龙的难受值的最小值。如果无法满足要求,输出-1。
题解:远古NOI的题还是有一点难度的呀,这题也是思考30min无果膜了别人的题解才做出来的
-1的情况只有一种:大头吃完了小头不够每人一个
首先我们会发现,如果有2个或2个以上的小头,是可以经过安排使小头不会加"难受"值的,
于是只用考虑大头和小头之间的关系,部分分告诉我们要多叉转二叉,于是可做
状态:dp[v][i][isbig]表示以v为根的子树中分i个给大头吃,根是否被大头吃(isbig=1 是 isbig=0 不是)的最小代价
因为只有二叉,很好转移
4.HDU1561 The More The Better
给一个森林,有点权,选一个点必须选它父亲,最多选k个点,求点权和最大值
题解:上课讲了啊OuO就是一个01背包,枚举的时候套一层枚举子树就好了
5.NOI2008道路设计
Z国坐落于遥远而又神奇的东方半岛上,在小Z的统治时代公路成为这里主要的交通手段。Z国共有n座城市,一 些城市之间由双向的公路所连接。非常神奇的是Z国的每个城市所处的经度都不相同,并且最多只和一个位于它东 边的城市直接通过公路相连。Z国的首都是Z国政治经济文化旅游的中心,每天都有成千上万的人从Z国的其他城市 涌向首都。为了使Z国的交通更加便利顺畅,小Z决定在Z国的公路系统中确定若干条规划路线,将其中的公路全部 x改建为铁路。我们定义每条规划路线为一个长度大于1的城市序列,每个城市在该序列中最多出现一次,序列中相 邻的城市之间由公路直接相连(待改建为铁路)。并且,每个城市最多只能出现在一条规划路线中,也就是说,任意 两条规划路线不能有公共部分。当然在一般情况下是不可能将所有的公路修建为铁路的,因此从有些城市出发去往 首都依然需要通过乘坐长途汽车,而长途汽车只往返于公路连接的相邻的城市之间,因此从某个城市出发可能需要 不断地换乘长途汽车和火车才能到达首都。我们定义一个城市的“不便利值”为从它出发到首都需要乘坐的长途汽 车的次数,而Z国的交通系统的“不便利值”为所有城市的不便利值的最大值,很明显首都的“不便利值”为0。小 Z想知道如何确定规划路线修建铁路使得Z国的交通系统的“不便利值”最小,以及有多少种不同的规划路线的选择 方案使得“不便利值”达到最小。当然方案总数可能非常大,小Z只关心这个天文数字modQ后的值。注意:规划路 线1-2-3和规划路线3-2-1是等价的,即将一条规划路线翻转依然认为是等价的。两个方案不同当且仅当其中一个方 案中存在一条规划路线不属于另一个方案。
题解:题解不重要,重要的是:为什么可以这么做
第一问直接dp[x][i]表示x点往下连i条链(i=0,1,2)就好了
第二问比较Tricky
“我们就算按树链剖分的方式搞这棵树,两个点也不会超过O(logn)条轻边”
所以我们给dp方程加一维:答案为j,然后让这个dp方程记方案数
dp[x][j][i]表示节点x往下连i条链,此时不方便值为j的方案数
因为j不超过O(logn)所以整体不超过O(nlogn)
小技巧:因为要取模还要判断可不可行,所以在计算中间结果的时候让%P=0的数%P=P
这样就少用了一个记录可不可行的数组
#include<cstdio> #include<cstring> #include<algorithm> const int maxn=200010,lim=10; #define ll long long using namespace std; int next[maxn],first[maxn],to[maxn],Cnt; int n,m,Q; ll f[maxn][12][3]; void add(int a,int b){next[++Cnt]=first[a],first[a]=Cnt,to[Cnt]=b;} int mo(ll t){return !t?0:t%Q?t%Q:Q;} void Trdp(int x,int fa) { int cnt=0; for(int i=first[x];i;i=next[i])if(to[i]!=fa)Trdp(to[i],x),cnt++; for(int i=0;i<=lim;i++)f[x][i][0]=1; if(!cnt)return; for (int i=first[x];i;i=next[i])if(to[i]!=fa) { int v=to[i]; for(int j=0;j<=lim;j++) { ll t,f1=!j?0:f[v][j-1][0]+f[v][j-1][1]+f[v][j-1][2],f2=f[v][j][0]+f[v][j][1]; //f1:修,f2:不修 t=(ll)f[x][j][2]*f1+(ll)f[x][j][1]*f2;f[x][j][2]=mo(t); t=(ll)f[x][j][1]*f1+(ll)f[x][j][0]*f2;f[x][j][1]=mo(t); t=(ll)f[x][j][0]*f1;f[x][j][0]=mo(t); } } } int main() { scanf("%d%d%d",&n,&m,&Q); for (int i=1,a,b;i<=m;i++) scanf("%d%d",&a,&b),add(a,b),add(b,a); if(m<n-1){puts("-1\n-1");return 0;} Trdp(1,0);ll sum; for(int i=0;i<=lim;i++) if (sum=f[1][i][0]+f[1][i][1]+f[1][i][2]) return printf("%d\n%d\n",i,(int)sum%Q),0; return 0; }
6.NOI2006 网络收费
——题面太长 省略 建议bzoj上的和codevs上的题面一起看——
题解:
又是一道远古NOI= =
首先它是一道语文题,收费规则是:对于某一个结点,若nA < nB则称之为A付费结点;反之则为B付费结点。
然后就可以分开算交的钱了
然后...其实就挺水的了 首先多叉转二叉,然后设dp[x][S][a]表示以x为根的子树中,它所有祖先的付费状态为S(一个二进制数,0为A,1为B),它的儿子有a个A
dp[x][S][A] = min{dp[lch][S|cur][Alch] + f[rch][S|cur][Arch]}。(cur表示x结点是A还是B)
边界情况是叶子节点
特殊考虑的是如果一个点的收费方式改变,它需要支付费用
然后就A了然后这题卡空间
注意到一个性质:一个点祖先多,儿子就少
所以把后两维压一起
说起来轻松,写起来...就400行出去了,我才不会告诉你我没有想到把S乘以一个数
这里贴一个网上的标程,毕竟自己写的我估计我自己过20天都看不懂了
//Lib #include<cstdio> #include<cstring> #include<cstdlib> #include<cmath> #include<ctime> #include<iostream> #include<algorithm> #include<vector> #include<string> #include<queue> using namespace std; //Macro #define rep(i,a,b) for(int i=a,tt=b;i<=tt;++i) #define drep(i,a,b) for(int i=a,tt=b;i>=tt;--i) #define erep(i,e,x) for(int i=x;i;i=e[i].next) #define irep(i,x) for(__typedef(x.begin()) i=x.begin();i!=x.end();i++) #define read() (strtol(ipos,&ipos,10)) #define sqr(x) ((x)*(x)) #define pb push_back #define PS system("pause"); typedef long long ll; typedef pair<int,int> pii; const int oo=~0U>>1; const double inf=1e100; const double eps=1e-6; string name="network",in=".in",out=".out"; //Var int n,m,ans=oo; int c[1100],flow[1100][1100],cost[1100][1100],fa[1100][1100]; bool type[1100]; struct T { int v[2100],base; int& operator ()(int i,int j){return v[i*base+j];} }f[2100]; void Init() { scanf("%d",&n);m=1<<n;int a; rep(i,1,m)scanf("%d",&a),type[i]=a==0; rep(i,1,m)scanf("%d",c+i); rep(i,1,m) rep(j,i+1,m) scanf("%d",&a),flow[i][j]=flow[j][i]=a; } int Calc(int x,int j,int k) { int ca=0,cb=0; rep(i,1,n) { if(k&1) ca+=cost[i][x]; else cb+=cost[i][x]; k>>=1; } if(j)return ca+(type[x]?0:c[x]); else return cb+(type[x]?c[x]:0); } int TDP(int i,int j,int k,int h) { int ret=f[i](j,k); if(ret)return ret; if(h) { ret=oo; int tmp,now,ls,tk; now=j<((1<<h)-j); tk=(k<<1)+now; ls=1<<h-1; for(int l=j-ls<0?0:j-ls;l<=j&&l<=ls;l++) { tmp=TDP(i<<1,l,tk,h-1)+TDP(i<<1^1,j-l,tk,h-1); ret=min(ret,tmp); } } else ret=Calc(i-m+1,j,k); f[i](j,k)=ret; return ret; } void Work() { rep(i,1,m) { int k=i+m-1; rep(j,1,n){k>>=1;fa[j][i]=k;} } rep(i,1,n+1) rep(j,1<<i-1,(1<<i)-1)f[j].base=1<<i-1; rep(i,1,m)rep(j,i+1,m)rep(k,1,n) if(fa[k][i]==fa[k][j]) { cost[k][i]+=flow[i][j], cost[k][j]+=flow[i][j]; break; } rep(i,0,m) ans=min(ans,TDP(1,i,0,n)); cout<<ans<<endl; } int main() { // freopen((name+in).c_str(),"r",stdin); // freopen((name+out).c_str(),"w",stdout); Init(); Work(); return 0; }
7.NOI2008奥运物流
题解:环套树dp的常规套路
先看树,再看环,然后把环套到树上
首先我们确定一个事情:修改一个点肯定是把它连到1节点上
对于环:我们手解方程(1h)
对于树:dp[x][i][j]表示节点x为根的子树,深度为i,修改j次获得最大值,dp即可(1h 30min)
环套到树上:我们手算环对答案的贡献(2h)
思考环需要套到哪(2h 30min)发现套到节点1贡献最大,具体手推
然后我们枚举1处的环有多长,dp即可(3h 30min)
如果你的手不是很健康 可以去网上搜论文 徐源盛《对一类动态规划问题的研究》里面有详细题解
#include<iostream> #include<cstdio> #include<algorithm> #include<cstdlib> #include<cstring> #include<cmath> using namespace std; double _MinT; #define Tryans(a) ((_MinT = (a)) > ans ? ans = _MinT : 1) const int maxn=70; const double inf=1e23; int first[maxn],to[maxn],next[maxn],cnt; double val[maxn]; double pw[maxn]; int r[maxn],size[maxn]; double dp[maxn][maxn][maxn]; double f[maxn][maxn]; int n,m; double k; inline void add(int a,int b){to[cnt]=b,next[cnt]=first[a],first[a]=cnt++;} inline void pre() { for(int i=0;i<=n;first[i++]=-1) for(int j=0;j<=n;j++) for(int k=0;k<=n;k++)dp[i][j][k]=-inf; cnt=0; //cout<<666; } inline void packdp(int x,int depth) { for(int i=0;i<=size[x];i++) for(int j=0;j<=m;j++)f[i][j]=-inf; f[0][0]=0.; for(int i=first[x],s=1;~i;i=next[i],s++) { for(int j=0;j<=m;j++) for(int k=0;k<=j;k++) f[s][j]=max(f[s][j],f[s-1][k]+dp[to[i]][j-k][depth]); } // cout<<666; } inline void Trdp(int x) { size[x]=0; for(int i=first[x];~i;i=next[i],size[x]++)Trdp(to[i]); packdp(x,2); for(int i=0;i<=n;i++) for(int j=1;j<=m;j++) dp[x][j][i]=f[size[x]][j-1]+val[x]*k; for(int i=0;i<=n;i++) { packdp(x,i+1); for(int j=0;j<=m;j++) dp[x][j][i]=max(dp[x][j][i],f[size[x]][j]+val[x]*pw[i]); } } int main() { scanf("%d%d%lf",&n,&m,&k); pw[0]=1,pw[1]=k; for(int i=1;i<=n;i++)scanf("%d",&r[i]); for(int i=1;i<=n;i++)scanf("%lf",&val[i]); for(int i=2;i<maxn;i++)pw[i]=pw[i-1]*k; double ans=-inf; for(int i=r[1],len=2,j;i^1;i=r[i],len++) { pre(); for(j=2;j<=n;j++) if(j^i) add(r[j],j);else add(1,j); Trdp(1); // cout<<666; Tryans(dp[1][m-(r[i]!=1)][0]/(1.-pw[len])); } printf("%.2lf",ans); }
8.bzoj2616 PERIODNI
题解:(OS:对着上课笔记写不就行了
这可能是我见过最假的树形dp
车的特性决定了它如果放在某一行,就相当于某一行某一列被一把剪子剪掉了,我们对于剩下的棋盘继续做dp,这样就可以转移了
#include<cstdio> #include<iostream> #include<algorithm> #define LL long long using namespace std; const int N = 1001, M = 1000001; const LL MOD = 1000000007; int n, K, root, G, H[N], h[N], len[N], son[N][2]; LL A[M], g[N][N], f[N][N]; void exgcd(LL a, LL b, LL &g, LL &x, LL &y) { if(!b) { g = a; x = 1LL; y = 0LL; return; } exgcd(b, a%b, g, y, x); y -= a/b*x; } void built(int &k, int l, int r) { int s = r; if(l > r) return; if(!k) k = ++G; for(int i = l; i < r; i++) if(H[i] < H[s]) s = i; len[k] = r-l+1; h[k] = H[s]; for(int i = l; i <= r; i++) H[i] -= h[k]; built(son[k][0], l, s-1); built(son[k][1], s+1, r); } LL calc(int a, int b, int k) { if(!k) return 1; if(a < k || b < k) return 0; LL fz = (A[a] * A[b]) % MOD; LL fm = (A[a-k] * A[b-k]) % MOD; LL y, x, g; fm = (fm * A[k]) % MOD; exgcd(fm, MOD, g, x, y); x = (x < 0) ? x += MOD : x; return (fz * x) % MOD; } void Dfs(int k) { g[k][0] = f[k][0] = 1; if(!k) return; Dfs(son[k][0]); Dfs(son[k][1]); for(int i = 1; i <= K; i++) for(int j = 0; j <= i; j++) g[k][i] = (g[k][i]+f[son[k][0]][j]*f[son[k][1]][i-j]) % MOD; for(int i = 1; i <= K; i++) for(int j = 0; j <= i; j++) f[k][i] = (f[k][i]+g[k][j]*calc(len[k]-j, h[k], i-j)) % MOD; } int main() { scanf("%d%d\n", &n, &K); A[0] = 1LL; for(int i = 1; i <= n; i++) scanf("%d", &H[i]); for(int i = 1; i <= M; i++) A[i] = ((LL)i * A[i-1]) % MOD; built(root, 1, n); Dfs(root); printf("%lld\n", f[root][K]); return 0; }
NOI2009 二叉查找树 正确性尚存疑
NOI2012 迷失游乐园 数学公式太多,待我学LaTeX归来再码