zoukankan      html  css  js  c++  java
  • 基础树形DP

    曾经的黑历史(

    有空了重构一下

    模板

    P1352 没有上司的舞会

    树状(dp)模板题。

    (f[i][0])为第(i)个人来了的方案数

    (f[i][1])为第(i)和人没来的方案数

    若第(i)个人来了,那么其下属均不回来

    若不来,其下属则有来和不来两种选择

    因此状态转移方程为:

    • (f[i][0]+=f[son][1])

    • (f[i][1]+=max(f[son][0],f[son][1]))

    P2015 二叉苹果树

    树上背包模板题

    每一个枝条都有"剪"和"不剪"两种可能

    把每一个儿子都看成一个"分组背包"

    (dp[i][j])表示第(i)个子树保留(j)条边

    每加入一个"儿子"后,枚举该"儿子"保留的边数,如图

    (ps:这里i-k后面还要减1是因为还要多保留从u->v这条边)

    故状态转移方程为:

    • (f[u][i]=max(f[v][i-k-1]+f[u][k]+w[u][v])(iin[1,m+1)])

    树上背包

    P2014 [CTSC1997]选课

    P1273 有线电视网

    P1270 “访问”美术馆

    P1272 重建道路


    P2014 [CTSC1997]选课

    和二叉苹果树一样的套路。

    把每一个子课程都看作是一个"分组背包",倒序枚举即可

    由于题目中可能有多棵树

    因此多开一个节点把所有"树根"连在一起

    同时,在倒序枚举时也要把这个新节点算进去

    转移方程:

    • (f[u][i]=max(f[v][i-k-1]+f[u][k]+w[v])(iin[1,m+1]))

    核心代码:

    void dp(int k){
    	vis[k]=1;
    	for(int i=0;i<son[k].size();i++){
    		if(vis[son[k][i]]!=1){
    			dp(son[k][i]);
    			for(int v=m+1;v>=1;v--){
    				for(int K=0;K<v;K++){
    					 f[k][v]=max(f[k][v],w[son[k][i]]+f[son[k][i]][K]+f[k][v-K-1]);
    				}
    			}
    		}
    	}
    	return;
    }
    

    P1273 有线电视网

    也是比较经典的一个树上背包问题

    题目中要求的是在不亏本的情况下最多的观看用户个数

    (f[i][j])表示第(i)个站传输给(j)个用户观看最终剩余的钱数

    若最终剩余钱数大于等于0,则说明未亏本

    反之,则说明亏本

    转移方程则为:

    • (f[u][i]=max(f[v][k]+f[u][i-k]-w[u][v]))

    (dp)完后从总人数开始倒序判断是否亏本即可

    贴个核心代码:

    (dp部分)

    void dfs(int x){
    	dp[x][0]=0;
    	if(val[x]){//如果是根节点
    		size[x]=1;//人数加一
    		dp[x][1]=val[x];
    		return;
    	}
    	for(int i=0;i<son[x].size();i++){
    		dfs(son[x][i]);
    		size[x]+=size[son[x][i]];//计算x节点下的人数总和
    		for(int j=size[x];j>=0;j--){//滚动数组,倒序枚举
    			for(int k=1;k<=size[son[x][i]];k++){//枚举子树传输的观众数量
    				dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[son[x][i]][k]-W[x][son[x][i]]);
    			}
    		}
    	}
    	return;
    }
    
    
    

    (判断部分)

         for(int i=m;i>=0;i--){
    	if(dp[1][i]>=0){//如果不亏本
    	cout<<i;
    	break;
    	}
    }
    

    P1270 “访问”美术馆

    跟P1273 有线电视网一样的套路

    (f[i][j])为在第(i)个节点下偷(j)幅画所需要的最小总时间

    状态转移方程也就呼之欲出了

    • (f[u][i]=min(f[v][k]+f[u][i-k]-2w[u][v]))

    这里(w[u][v])要乘2是因为要进出各一趟

    核心代码:

    int dfs(int x){
    	if(paint[x]!=0){
    		return paint[x];
    	}
    	int s=0;
    	for(int i=0;i<son[x].size();i++){
    		
    		int v = son[x][i];
    		int t=dfs(v);
    		s+=t;
    		for(int j =s;j>0;j--){
    			for(int k=0;k<=t;k++){
    				dp[x][j] = min(dp[x][j] , dp[v][k] + dp[x][j-k]+w[x][v]*2);
    			}
    		}
    	}
    	return s;
    }
    

    P1272 重建道路

    同样也是一道比较经典的树上背包问题

    (f[i][j])为第(i)个节点断出一个大小为(j)的子树所需要的断开总数

    状态转移方程:

    • (f[u][i]=min(f[v][k]+f[u][i-k]-1))

    ((v)为根的子树提供(k)个节点,(u)和其他儿子提供(j-k)个节点)

    同时,由于一开始时一个子树都没有加进来

    即把(u)的所有"儿子"都切断了

    因此当把(v)儿子加进来的时候要把之前那段减去的边加回来

    核心代码:

    void dfs(int x){
    	size[x] = 1;
    	if(!is_son[x]){
    		dp[x][1] = 0;
    		size[x] = 1;
    		return;
    	}
    	for(int i=0;i<son[x].size();i++){
    		int v = son[x][i];
    		dfs(v);
    		size[x]+=size[v];
    		for(int j = size[x];j>=0;j--){
    			for(int k = 1;k<=size[v];k++){//这里题解里很多人都写成了<j,问题是子树可能本身就没有这么多子节点,感觉有些问题
    				dp[x][j] = min(dp[x][j],dp[x][j-k]+dp[v][k]-1);
    			}
    		}
    	}
    }
    

    普通树形(DP)

    P2016 战略游戏

    P2458 [SDOI2006]保安站岗 题解

    P4084 [USACO17DEC]Barn Painting G

    P2585 [ZJOI] 三色二叉树

    P2279 消防局的设立


    P2016 战略游戏

    带了点贪心思想的树形(DP)

    如果父节点放了一个守卫

    那其子节点就都不用放守卫了

    反之,子节点都要放一个守卫

    转移方程:

    • (f[u][0]+=f[v][1])
    • (f[u][1]+=min(f[v][1],f[v][0]))

    为什么不用儿子的儿子("孙子")节点来看守儿子节点?

    如果一个节点不是叶子节点,那他的子节点数必定大于或等于(1),因此如果用儿子节点来看守其父节点,花费的数量肯定会更多(或不变)。

    遗憾的是题解里似乎没人说正确性的证明?,还是说太简单了都懒得证了

    核心代码:

    void dfs(int x){
       if(x!=1509)
    	dp[x][1]=1,dp[x][0]=0;
    	for(int i=0;i<son[x].size();i++){
    		dfs(son[x][i]);
    		dp[x][0]+=dp[son[x][i]][1];
    		dp[x][1]+=min(dp[son[x][i]][1],dp[son[x][i]][0]);		
    	}
    }
    

    关于这道题目的带点权版:

    P2458 [SDOI2006]保安站岗

    题解链接

    P4084 [USACO17DEC]Barn Painting G

    树上(DP)求方案数。

    还算是比较简单的题目吧...

    设:

    (f[i][0])为第(i)个节点涂红色的方案数

    (f[i][1])为第(i)个节点涂绿色的方案数

    (f[i][2])为第(i)个节点涂蓝色的方案数

    假设第(i)号节点涂了红色,那么它的上一个节点就只能涂绿色和蓝色

    其他情况也同理

    用乘法定理乘一下即可。

    转移方程:

    • (egin{cases}f[u][1]=f[u][1]*((f[v][2]+f[v][3]))\f[u][2]=f[u][2]*((f[v][1]+f[v][3]))\f[u][3]=f[u][3]*((f[v][1]+f[v][2]))end{cases})

    换根(DP)

    一种形式十分优美的树形(DP)

    P3478 [POI2008]STA-Station

    P2986 [USACO10MAR]Great Cow Gathering G

    P3047 [Nearby Cows G]Great Cow Gathering G

    CF708C Centroids

    CF1187E Tree Painting


    P3478 [POI2008]STA-Station

    换根DP的模板题。

    这里我们设

    • (size[i])为以(1)为根节点时节点(i)的子树大小

    • (dep[i])为以(1)为根节点时节点(i)的深度大小

    • (dp[i])为以(i)为根节点时深度之和的大小

    很明显,我们可以通过一遍DFS求出以(1)为根节点时的深度之和

    如果一个个的去算的话

    照这个数据范围,显然会T飞

    这个时候就要用到换根DP了

    换根(DP)优化

    可以看出,当我们把根节点从1换到3时

    对子节点3的贡献由两部分组成

    1.自己子树的贡献(图中的k)

    2.父亲节点(1)的贡献


    如何转移

    • 首先是(k),作为自己子树所产生的贡献肯定要加上

    • (dp[u])为以(u)为根节点时的深度总值,在计算时,要减去(v)的子树所产生的贡献,不然就重复计算了,同时

    在以 (u)为根时,v节点及其子树内的所有节点的深度都增加了(1),需要减去

    (图中红色的节点)

    合起来就是(dp[u]-(size[v]+k))

    • 除v子树外的其他节点也一样

    在以(v)为根时,除(v)节点及其子树外的其他节点的深度都增加了(1)

    (图中蓝色的节点)

    合起来就是((size[1]-size[v]))

    得到转移方程

    • (dp[v] = k+(dp[u]-(k+size[v]))+(size[1]-size[v]))

    化简一下

    • (dp[v] = dp[u]-2size[v]+size[1])

    核心代码:

    void dfs1(int x){
    	size[x] = 1;
        vis[x] = 1;
    	for(int i=0;i<son[x].size();i++){
    		int v = son[x][i];
    		if(!vis[v]){
    		dep[v] = dep[x] +1;
    		dfs1(v);
    		size[x]+=size[v];	
    		}
    			
    	}
    }
    void dfs2(int x){
        vis[x] = 1;
    	for(int i=0;i<son[x].size();i++){
    		int v = son[x][i];
    		if(!vis[v]){
    		dp[v] = dp[x] +size[1] - 2*size[v];
    		dfs2(v);	
    		}
    	}
    }
    

    P2986 [USACO10MAR]Great Cow Gathering G

    前面那道题目的带权值版

    一模一样的思路,只需要把状态转移方程转换一下即可。

     void dfs(int u,int fa){
    	
    	
        for(int i=head[u];i;i=edge[i].next){
        	int v =edge[i].v;
        	if(v==fa) continue;
        	dfs(v,u);
        	size[u] += size[v];
        	sum[u]+=(sum[v]+edge[i].w*size[v]);	
    	}
    }
    
     void dp(int u,int fa){
    	for(int i=head[u];i;i=edge[i].next){
        	   int v =edge[i].v;
    			   	if(v==fa) continue;
    			f[v] = 1LL*f[u] + AN*edge[i].w - 2*size[v]*edge[i].w;
    			ans = min(ans,f[v]);
    			dp(v,u);
    	}
    }
    

    P3047 [Nearby Cows G]

    1.(状态表示)

    (size[i][j])为第i个节点向下(j)层所包含的点权和

    (f[i][j])为第(i)个点距离它不超过 (j)的所有节点权值和

    2.状态转移

    对于(size[i][j]:)

    (size[u][j] =sum size[v][j-1]) 自己向下(j)层即为儿子向下(j-1)

    对于(f[i][j]:)

    儿子对它的贡献:

    (size[v][j])

    自己向下(j)层,儿子节点肯定也要向下(j)

    父亲对它的贡献:

    (f[u][j-1]-size[v][j-2])

    父亲节点扩展(j-1)层的值减去和儿子节点的值所重复包含的(j-2)层值

    转移方程:

    (f[v][j] = f[u][j-1]+size[v][j]-size[v][j-2])

    核心代码:

    void dfs(int u,int fa){
    	
    	for(int i=head[u];i;i=edge[i].next){
    		int v =edge[i].v;
    		if(v==fa) continue;
    		dep[v]=dep[u]+1;
    		dfs(v,u);
    		for(int i=1;i<=k;i++){
    			size[u][i]+=size[v][i-1];
    		}
    	}
    }
    void dp(int u,int fa){
    	
    	for(int i=head[u];i;i=edge[i].next){
    		int v=edge[i].v;
    		if(v==fa) continue;
    
    		for(int i=1;i<=k;i++){
    			if(i-2>=0)
    			f[v][i] = size[v][i]+f[u][i-1] - size[v][i-2];
    			else f[v][i] = size[v][i]+f[u][i-1];
    		}
    			
    		dp(v,u);
    	}
    }
    

    CF708C Centroids

    一道做起来比较麻烦的换根(DP)

    分析

    首先对于一个节点来说,大小大于(n/2)的节点肯定只有一个,这个显而易见

    再来看如何改造

    如果说该节点本身的重儿子就小于(n/2),那肯定可以成为树的重心

    反之,肯定要在重儿子里找出一个重量最大的且小于等于(n/2)的子树,并将其断开,连接到根节点上(相当于删去这颗子树)

    如果重儿子的大小减去被删去儿子的大小小于等于(n/2),则说明可以改造

    反之,无法改造

    如何转移

    分两种情况来讨论

    (1).该节点不是其父亲节点重儿子

    其父节点的重儿子不会被改变,只需要判断该节点的重儿子是否改成其父节点即可

    (2).该节点是其父亲节点的重儿子

    其父亲节点的重儿子会变为其"次大"儿子,其儿子节点的重儿子不会改变

    核心代码:

    void dfs(int u){
    	vis[u] = 1;
    	size[u] = 1;
    	for(int i=0;i<son[u].size();i++){
    		int v = son[u][i];
    		if(!vis[v]){
    		dfs(v);
    		size[u]+=size[v];
    		if(size[v] > size[maxson[u]])
    			maxson[u] = v;	
    	  }	
    	 }
    	   
    			if(maxson[u]!=0){
    				
    				if(size[maxson[u]]<=n/2) dp[u] = size[maxson[u]]; 
    				else dp[u] = dp[maxson[u]];
    			}
    }
    
    
    void exchange(int u,int v){
            size[u] = size[u] - size[v];
    		size[v] = size[v] + size[u];
    		if(v==maxson[u]){
    			maxson[u] = 0;
    			for(int i=0;i<son[u].size();i++){
    				int V = son[u][i];
    				if(V!=v&&size[V] > size[maxson[u]]){
    					maxson[u] = V;
    				} 
    			}
    			if(maxson[u]!=0){
    				
    				if(size[maxson[u]]<=n/2) dp[u] = size[maxson[u]];
    				else dp[u] = dp[maxson[u]];
    			}
    		}
    		if(size[maxson[v]]<size[u]){
    			maxson[v] = u;
    			if(maxson[v]!=0){
    				
    				if(size[maxson[v]]<=n/2) dp[v] = size[maxson[v]];
    				else dp[v] = dp[maxson[v]];
    			}
    		}	
    }
    void dfs2(int u){
    	vis[u]  = 1;
        
    	 if(size[maxson[u]]<=n/2||size[maxson[u]] - dp[maxson[u]]<=n/2) ans[u]=1;
    	for(int i=0;i<son[u].size();i++){
    		int v = son[u][i];
    		if(!vis[v]){
    			exchange(u,v);
    			dfs2(v);
    			exchange(v,u);
    		}
    	}
    }
    

    end.

    基环树部分还是先缓缓吧,暂时还未完全掌握

  • 相关阅读:
    dubbo的防痴呆设计
    dubbo设计实现的健壮性
    Dubbo配置设计
    一些设计上的常识
    如何使用sqlalchemy获取某年某月的数据总和
    使用datatables实现后台分页功能,减轻前端渲染压力
    datatable使用介绍
    ssh远程执行nohup命令不退出
    ssh 执行多条命令包含awk的用法
    sys.argv的妙用:python命令行参数列表的修改、增加、删除
  • 原文地址:https://www.cnblogs.com/xcxc82/p/13348123.html
Copyright © 2011-2022 走看看