与树相关的题目 树形DP 总结,不断汇总中

本文深入讲解树形动态规划的基础概念及应用案例,包括经典题目解析、分组背包思想的应用、树的分治算法等,旨在帮助读者理解并掌握树形DP的精髓。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 hdu 1561     The more, The Better   树形DP入门   利用分组背包的思想

#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
int dp[250][250];
vector<int> edge[250];
int n,m;
int val[250];
int max(int a,int b){
	return a>b?a:b;
}
/*
/////////////////////////////////////////////
分组背包:
for 所有的组i
    for v=V..0
        for 所有的k属于组i
            f[v]=max{f[v],f[v-c[k]]+w[k]}
*/
void dfs(int u,int fa){//用分组背包的思想更易于理解以及掌握
	int i,j,k;
    dp[u][1]=val[u];
	for(i=0;i<edge[u].size();i++)//枚举每一组背包,即每一个儿子,每组背包至少去一个物品
	{
		int v=edge[u][i];
		if(v==fa) continue;
		dfs(v,u);
		int t=1;
		if(u==0) t=0;//u为根节点时不需要分配背包容量
		for(k=m;k>=1;k--)//枚举背包容量,dp[v][1....k]的状态(已经得出的最优状态)为v儿子这组背包的k个物品
		{
			for(j=1;j<=k-t;j++)//枚举第i组的每个物品,即第i个儿子的所有状态
			{    
				//u为根节点时子节点可得到的最大分配为枚举的背包容量,否则要减1
				if(dp[u][k-j]!=-1&&dp[v][j]!=-1)
				dp[u][k]=max(dp[u][k],dp[u][k-j]+dp[v][j]);//枚举每个物品,找到能让dp[u][k]更优的物品
                /*
				注意这里的讲究:
				j递增,k-j递减,所以保证了每次如果dp[u][k]被更新的话dp[u][k-j]的状态都是原来的状态(没有被v更新过),
				也保证了dp[u][k]是最多取一个物品来更新的,*_* 哈哈,终于把每个细节搞通了,ORZ myself
				*/
			}
			//其实就相当于用k-1的容量(u为0时是k)去分配给所有的儿子,得出最优的分配方案,再更新dp[u][k];
		}
	}
}
int  main()
{
	int i,a,b;
	while(scanf("%d%d",&n,&m),(n||m))
	{
		for(i=0;i<=n;i++) edge[i].clear();
		val[0]=0;
		for(i=1;i<=n;i++)
		{
			scanf("%d%d",&a,&b);
			edge[a].push_back(i);
			val[i]=b;
		}
	    memset(dp,-1,sizeof(dp));dp[0][0]=0;
		dfs(0,-1);
		printf("%d\n",dp[0][m]);
	}
	return 0;
}

另一种写法

View Code
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
int dp[250][250];
vector<int> edge[250];
int n,m;
int val[250];
int max(int a,int b){
    return a>b?a:b;
}
/*
/////////////////////////////////////////////
分组背包:
for 所有的组i
    for v=V..0
        for 所有的k属于组i
            f[v]=max{f[v],f[v-c[k]]+w[k]}
*/
void dfs(int u,int fa){//用分组背包的思想更易于理解以及掌握
    int i,j,k;
    dp[u][1]=val[u];
    for(i=0;i<edge[u].size();i++)//枚举每一组背包,即每一个儿子,每组背包至少去一个物品
    {
        int v=edge[u][i];
        if(v==fa) continue;
        dfs(v,u);
        int t=1;
        if(u==0) t=0;
        for(k=m;k>=1;k--)
         {
            for(j=t;j<=k-1;j++)//也相当于是枚举第i组背包的每个物品
            {
                if(dp[u][j]!=-1&&dp[v][k-j]!=-1)
                dp[u][k]=max(dp[u][k],dp[u][j]+dp[v][k-j]);//枚举每个物品,找到能让dp[u][k]更优的物品
            }
        }
    }
}
int  main()
{
    int i,a,b;
    while(scanf("%d%d",&n,&m),(n||m))
    {
        for(i=0;i<=n;i++) edge[i].clear();
        val[0]=0;
        for(i=1;i<=n;i++)
        {
            scanf("%d%d",&a,&b);
            edge[a].push_back(i);
            val[i]=b;
        }
        memset(dp,-1,sizeof(dp));dp[0][0]=0;
        dfs(0,-1);
        printf("%d\n",dp[0][m]);
    }
    return 0;
}

 

  

hdu 4003  Find Metal Mineral

/*
还是分组背包的思想,与上一题差不多
不过这个题目有一个特点,花费的是边权
所以每次分配背包容量时,都要加上相应的边权消耗
dp[i][j]表示以i为根的子树,放j个机器人去遍历(不回来)的最小花费
dp[i][0]就表示没有机器人停留在子树中,即派了一个机器人下去,
遍历完后又回来了,其实就等于子树中边权值和的两倍
*/
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
vector<pair<int,int> > edge[10010];
int dp[10010][15];
int m;
int min(int a,int b){return a>b?b:a;}
void dfs(int u,int fa){
	int i,j,k;
	for(i=0;i<edge[u].size();i++){
		int v=edge[u][i].first;
		if(v==fa) continue;
		dfs(v,u);
		for(k=m;k>=0;k--){
			dp[u][k]+=dp[v][0]+2*edge[u][i].second;//dp[v][0]表示遍历v子树后回来,即没有机器人停留在子树中
			for(j=1;j<=k;j++)
				dp[u][k]=min(dp[u][k],dp[u][k-j]+dp[v][j]+j*edge[u][i].second);//派j个机器人给v子树就会经过u-v之间的边j次
		}
	}
}
int main(){
	int n,s,i,a,b,w;
	while(scanf("%d%d%d",&n,&s,&m)!=EOF){
		for(i=0;i<=n;i++) edge[i].clear();
		for(i=0;i<n-1;i++){
			scanf("%d%d%d",&a,&b,&w);
			edge[a].push_back(make_pair(b,w));
			edge[b].push_back(make_pair(a,w));
		}
		memset(dp,0,sizeof(dp));
		dfs(s,0);
		printf("%d\n",dp[s][m]);
	}
}

 poj 1849 Two   类似于上面一题,m=2

View Code
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
vector<pair<int,int> > edge[10010];
int dp[10010][15];
int m;
int min(int a,int b){return a>b?b:a;}
void dfs(int u,int fa){
int i,j,k;
for(i=0;i<edge[u].size();i++){
int v=edge[u][i].first;
if(v==fa) continue;
dfs(v,u);
for(k=m;k>=0;k--){
dp[u][k]+=dp[v][0]+2*edge[u][i].second;
for(j=1;j<=k;j++)
dp[u][k]=min(dp[u][k],dp[u][k-j]+dp[v][j]+j*edge[u][i].second);
}
}
}
int main(){
int n,s,i,a,b,w;
while(scanf("%d%d",&n,&s)!=EOF){
m=2;
for(i=0;i<=n;i++) edge[i].clear();
for(i=0;i<n-1;i++){
scanf("%d%d%d",&a,&b,&w);
edge[a].push_back(make_pair(b,w));
edge[b].push_back(make_pair(a,w));
}
memset(dp,0,sizeof(dp));
dfs(s,0);
printf("%d\n",dp[s][m]);
}
}

 

http://www.codeforces.com/problemset/problem/161/D

判断树中有多少的长度为k的简单路径dp[i][j]记录以i为根长度为j的路径数(i为路径的端点) ,每次回溯上来的时候维护dp[i][1--k]的值就好了

View Code
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
typedef __int64 lld;
lld dp[50010][510];
lld ans;
vector<int> g[50010];
int k;
void dfs(int u,int fa)
{
    int i,j;
    dp[u][0]=1;
    for(i=0;i<g[u].size();i++)
    {
        int v=g[u][i];
        if(v==fa) continue;
        dfs(v,u);
        for(j=1;j<=k;j++) ans+=dp[u][j-1]*dp[v][k-j];
        for(j=1;j<=k;j++) dp[u][j]+=dp[v][j-1];
    }
}
int main()
{
    int n,i,j,a,b;
    scanf("%d%d",&n,&k);
    for(i=0;i<n-1;i++)
    {
        scanf("%d%d",&a,&b);
        g[a].push_back(b);
        g[b].push_back(a);
    }
    memset(dp,0,sizeof(dp));
    ans=0;
    dfs(1,0);
    printf("%I64d\n",ans);
    return 0;
}

 


hdu 3586 Information Disturbing

View Code
//去掉一些边使得根节点不与叶子节点连通,在去掉的总的边权<=m的前提下
//使得所有边中最大的边权尽可能的小
#include<cstdio>
#include<cstring>

const int INF = 100000;
const int maxn = 1010;

struct node{
    int v,w,next;
}edge[100010];

int head[maxn];
int tot;
void add(int u,int v,int w)
{
    edge[tot].v=v;
    edge[tot].w=w;
    edge[tot].next=head[u];
    head[u]=tot++;
}
//二分判可行
//dfs 求出 子树中去掉的边权之和,如果大于u->v之间的边权,则改为去u->之间的边
//然后一层层回溯上去就好了
int dfs(int u,int fa,int num)
{
    int sum=0;
    bool flag=false;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        if(edge[i].v==fa) continue;
        int s=dfs(edge[i].v,u,num);
        if(s>edge[i].w&&edge[i].w<=num)
        {
            s=edge[i].w;
        }
        sum+=s;
        flag=true;
    }
    if(!flag) return INF;
    return sum;
}
int main()
{
     int n,m,i,j,a,b,c;
     while(scanf("%d%d",&n,&m),(n||m))
     {
         tot=0;
         memset(head,-1,sizeof(head));
         for(i=0;i<n-1;i++)
         {
             scanf("%d%d%d",&a,&b,&c);
             add(a,b,c);
             add(b,a,c);
         }
         int l=1,r=1001;
         int ans=-1;
         while(l<=r)
         {
             int mid=(l+r)>>1;    
             if(dfs(1,1,mid)<=m)
             {
                    ans=mid;
                    r=mid-1;
             }
             else l=mid+1;
         }
         printf("%d\n",ans);
     }
}

 

 

zoj 3506 cut the tree

View Code
/*
 题意:给出一颗树,每个节点都有一个权值,每次可以砍掉一条边,然后你可以在分开的两个集合中选一个,
 求恰好砍m次能获得的最大权值和最小权值
 
 这题的关键在于细节处理,+1,-1都要很小心
 di[i]表示以i为根的子树的边的数量
dp[i][j]代表以i为根的子树砍j刀所能获得的最小权值,最大权值的话只需将所有点的权值变成负的再dfs即可
求完dp后,要进行简单的转换才是答案
假设最后剩下的节点属于i为根的子树,则i与父节点之间的边肯定是被砍掉的
用dp[i][j]来更新答案时,需要先算出j的范围,即以i为根的子树至少需要砍几刀,最多能够砍几刀
树边有n-1条,以i为根的子树最多有di【i]条边,总共要砍m条边
所以i子树外的其他边最多能够砍 n-1-di[i] 条,所以i子树至少要砍m-(n-1-di[i])条,小于0时就是0了
最多能砍min(di[i],m-1)条,因为i与父节点之间的边被砍掉了
 */
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int INF = 100000000;
const int maxn = 1010;
int dp[maxn][30];
vector<int> g[maxn];
int val[maxn];
int di[maxn];
int n, m;
int min(int a, int b) {
    return a < b ? a : b;
}
int max(int a, int b) {
    return a > b ? a : b;
}
void dfs(int u, int fa) {
    int i, j, k;
    dp[u][0] = val[u];//在第一次遍历到u的时候先记录u节点的权值,待回溯的时候再加上
    di[u] = 0;
    for (i = 1; i <= m; i++)
        dp[u][i] = INF;
    for (i = 0; i < (int) g[u].size(); i++) {
        int v = g[u][i];
        if (v == fa)
            continue;
        dfs(v, u);
        di[u] += di[v] + 1;
        for (k = m; k >= 0; k--) {
            dp[u][k] += dp[v][0];//一刀都不砍
            for (j = 1; j <= k; j++)//保留u->v之间的边
                dp[u][k] = min(dp[u][k], dp[v][j] + dp[u][k - j]);
            for (j = 1; j <= di[v] + 1 && j <= k; j++)//砍掉u->v之间的边
                dp[u][k] = min(dp[u][k], dp[u][k - j]);
        }
    }
}
int solve() {
    dfs(1, 0);
    int ans = dp[1][m];
    for (int i = 2; i <= n; i++) {
        for (int j = max(0, m - n + di[i]+1); j <= di[i] && j < m; j++)//i与父节点之间的边一定砍掉(j<m)
            ans = min(ans, dp[i][j]);//i子树的外面 至多 能砍 (n-1-di[i])刀,其他的刀必须要在i子树内砍
    }
    return ans;
}
int main() {
    int i, j, a, b;
    while (scanf("%d%d", &n, &m) != EOF) {
        for (i = 1; i <= n; i++)
            g[i].clear();
        for (i = 1; i <= n; i++)
            scanf("%d", &val[i]);
        for (i = 1; i <= n - 1; i++) {
            scanf("%d%d", &a, &b);
            g[a].push_back(b);
            g[b].push_back(a);
        }
        printf("%d ", solve());
        for (i = 1; i <= n; i++)
            val[i] = -val[i];
        printf("%d\n", -solve());
    }
}

 

 poj 1155 TELE

View Code
/*
DP[i][j]表示以i为根节点的子树总共使j个客户收到信息的最大报酬
dp[u][j+k]=max(dp[u][j+k],dp[u][j]+dp[v][k]-w) u是v的父节点,w为u、v的边权
*/
#include<cstdio>
#include<cstring>
#define max(a,b) a>b?a:b
const int INF = 1000000000;
const int maxn = 3010;
int head[maxn],dp[maxn][maxn],n,m;
int num[maxn];
struct EDGE{
    int v,w,next;
}edge[maxn];
int  tot;
void add_edge(int s,int t,int w){
    edge[tot].v=t;
    edge[tot].w=w;
    edge[tot].next=head[s];
    head[s]=tot++;
}
void dfs(int u,int fa){
    int i,j,k,tmp[maxn];
    for(i=head[u];i!=-1;i=edge[i].next){
        int v=edge[i].v;
        if(v==fa) continue;
        dfs(v,u);
        for(j=0;j<=num[u];j++)
            tmp[j]=dp[u][j];//先预存下所有的最优值,因为在下面的更新过程中有可能会被更新掉
         for(j = 0; j <= num[u]; j ++)
            for(k = 1; k <= num[v]; k ++)
                dp[u][j+k] = max(dp[u][j+k], tmp[j]+dp[v][k]-edge[i].w);
        num[u] += num[v];//利用回溯,自底向上的进行DP
    }
}
int main(){
    int i,j,k,a,c;
    tot=0;
    memset(head,-1,sizeof(head));
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
            dp[i][j]=-INF;
    for(i=1;i<=n-m;i++){
        scanf("%d",&k);
        for(j=0;j<k;j++){
            scanf("%d%d",&a,&c);
            add_edge(i,a,c);
        }
    }
    memset(num,0,sizeof(num));
    for(i=n-m+1;i<=n;i++){
        num[i]=1;
        scanf("%d",&dp[i][1]);
    }
    dfs(1,0);
    for(i=m;i>=0;i--){
        if(dp[1][i]>=0){//输出最多的可能的用户数,在不亏本的前提下
            printf("%d\n",i);
            break;
        }
    }
    return 0;
}

 

poj 3107

View Code
/*
求一棵树分别去掉哪些点后可以使得剩下的点集中点数最多的连通块的点数最小
直接进行一次DFS求解即可
*/

#include<cstdio>
#include<cstring>
const int maxn = 50010;
struct node{
    int v,next;
}edge[maxn*2];
int head[maxn];
int E,ans,n;
void add(int a,int b)
{
    edge[E].v=b;
    edge[E].next=head[a];
    head[a]=E++;
}
inline int max(int a,int b){
    return a>b?a:b;
}
inline int min(int a,int b){
    return a<b?a:b;
}
int res[maxn];
int dfs(int u,int fa)
{
    int i,Max=0;
    int s=1;
    for(i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(v==fa) continue;
        int t=dfs(v,u);
        Max=max(Max,t);
        s+=t;
    }
    Max=max(Max,n-s);
    res[u]=Max;
    ans=min(ans,Max);
    return s;
}
int main()
{
    int i,j,a,b;
    while(scanf("%d",&n)!=EOF)
    {
        E=0;ans=100000000;
        for(i=1;i<=n;i++) head[i]=-1;
        for(i=1;i<n;i++)
        {
            scanf("%d%d",&a,&b);
            add(a,b);
            add(b,a);
        }
        dfs(1,0);
        for(i=1;i<=n;i++)
        {
            if(res[i]==ans)
                printf("%d ",i);
        }
        puts("");
    }
    return 0;
}

 

poj 1655

View Code
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
vector<int> edge[20010];
int son[20010];
int ans[20010];
int max(int a,int b){
    return a>b?a:b;
}
void dfs(int u,int fa)
{
    son[u]=1;
    ans[u]=1;
    for(int i=0;i<edge[u].size();i++)
    {
        int to=edge[u][i];
        if(to==fa) continue;
        dfs(to,u);
        son[u]+=son[to];
        ans[u]=max(ans[u],son[to]);
    }
}
int main()
{
    int t,n,i,j,a,b;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d",&n);
        for(i=0;i<=n;i++) edge[i].clear();
        for(i=1;i<n;i++)
        {
            scanf("%d%d",&a,&b);
            edge[a].push_back(b);
            edge[b].push_back(a);
        }
        dfs(1,0);
        int ret=~0u>>1;
        int id;
        for(i=1;i<=n;i++)
        {
            if(max(ans[i],n-son[i])<ret) 
            {
                ret=max(ans[i],n-son[i]);
                id=i;
            }
        }
        printf("%d %d\n",id,ret);

    }
}

 

poj 1741  树的分治算法  买一送一(poj 1987)

View Code
/*
详见09年国家队论文 《分治算法在树的路径问题中的应用》
每次先找到重心,可以用反证法证明以重心为根的子树中的节点数不超过n/2
所以每次在子树中重新找重心的时候都把问题的规模缩小了1/2即分治了,
这样子树的深度就不会超过log(n)
*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 10010;
bool used[maxn];
int head[maxn], son[maxn] , d[maxn] , ans[maxn];
struct node {
    int v,w,next;
}edge[2*maxn];
int tot,Max,root,cnt,m,n;
void add(int a,int b,int w)
{
    edge[tot].v=b;
    edge[tot].w=w;
    edge[tot].next=head[a];
    head[a]=tot++;
}
void init()
{
    memset(used,false,sizeof(used));
    memset(head,-1,sizeof(head));
    int a,b,w;tot=0;
    for(int i=1;i<n;i++) 
    {
        scanf("%d%d%d",&a,&b,&w);
        add(a,b,w);
        add(b,a,w);
    }
}
void  get_root(int u,int fa,int size)//poj 1695 实现的功能 找重心
{
    int mx=-1;
    son[u]=1;
    for(int i=head[u],to;i!=-1;i=edge[i].next)
    {
        to=edge[i].v;
        if(used[to] || to==fa) continue;
        get_root(to,u,size);
        son[u]+=son[to];
        if(son[to]>mx) mx=son[to];
    }
    if(size-son[u]>mx) mx=size-son[u];
    if(mx<Max) Max=mx,root=u; 
}
void get_dis(int u,int len,int fa)
{
    d[++cnt]=len;
    for(int i=head[u],to;i!=-1;i=edge[i].next)
    {
       to=edge[i].v;
       if(used[to] || to==fa) continue;
       get_dis(to,len+edge[i].w,u);
    }
}
int calc(int u,int len)//nlog(n)
{
    cnt=0;
    int sum=0;
    get_dis(u,len,-1);
    sort(d+1,d+cnt+1);
    for(int i=1,j=cnt;i<=j;i++)
    {
        while(d[i]+d[j]>m && i<=j) j--;
        if(i<j) sum+=j-i;
    }
    return sum;
}
int find_root(int u)
{
    Max=son[u];
    get_root(u,0,Max);
    return root;
}
void solve(int u)//可以证明搜索树的深度上界为log(n)
{
    u=find_root(u);
    ans[u]=calc(u,0);
    used[u]=true;
    for(int i=head[u],to;i!=-1;i=edge[i].next)
    {
        to=edge[i].v;
        if(used[to]) continue;
        ans[u]-=calc(to,edge[i].w);
        solve(to);
    }
}
int main()
{
    while(scanf("%d%d",&n,&m)!=EOF && n)
    {
        init();
        son[1]=n;
        solve(1);
        int res=0;
        for(int i=1;i<=n;i++) res+=ans[i];
        printf("%d\n",res);
    }
    return 0;
}

 

 

poj 2378

View Code
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
const int maxn = 10010;
vector<int> edge[maxn];
int sum[maxn],ans[maxn],n;
void dfs(int u,int fa){
     sum[u]=1;
     int i,j; 
     bool flag=true;
     for(i=0;i<edge[u].size();i++){
         int to=edge[u][i];
         if(to==fa) continue ;
         dfs(to,u);
         if(sum[to]>n/2) flag=false;
         sum[u]+=sum[to];
     }
     if(flag&&n-sum[u]<=n/2) ans[u]=true;
}
int main(){
    int i,a,b;
    while(scanf("%d",&n)!=EOF){
        memset(ans,0,sizeof(ans));
        for(i=0;i<=n;i++) edge[i].clear();
        for(i=0;i<n-1;i++){
            scanf("%d%d",&a,&b);
            edge[a].push_back(b);
            edge[b].push_back(a);
        }
        dfs(1,0);
        for(i=1;i<=n;i++){
            if(ans[i])
                printf("%d\n",i);
        }
        puts("");
    }
    return 0;
}

 

 二分+树形DP+状态压缩 http://www.bnuoj.com/bnuoj/problem_show.php?pid=17189

View Code
/*

给出一棵树,每个节点都有一个质量,一种颜色,
总共有7种颜色,每条边都有一个花费v,砍断一条边可以获得该子树所有的节点
但是要花费v的能量,总能量为tot
最后问你在收集满所有的颜色种类的前提下
能获得的最大的质量的果子是多少(所有获得的果子中最小的质量)
二分答案,判断通过质量大于等于mid的所有节点能否完成目标
dp[i][j]表示i子树颜色状态为j时的最小花费
sum[i]表示i子树总的颜色状态
*/

#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int inf = 0x3f3f3f3f;
const int M = 110;
int dp[M][(1<<7)+1];
int sum[M],col[M],cut[M],qual[M];
vector<int> edge[M];
void Min(int &a,int b){
    if(b<a) a=b;
}
void dfs(int u,int fa,int mid)
{
    memset(dp[u],0x3f,sizeof(dp[u]));
    dp[u][0]=0;sum[u]=0;
    for(int i=0;i<edge[u].size();i++)
    {
        int v=edge[u][i];
        if(v==fa) continue;
        dfs(v,u,mid);
        sum[u]|=sum[v];
        for(int j=0;j<(1<<7);j++)
        {
            if(dp[u][j]==inf) continue;
            for(int k=0;k<(1<<7);k++)
                Min(dp[u][j|k],dp[u][j]+dp[v][k]);
        }
    }
    if(qual[u]>=mid) sum[u]|=(1<<col[u]);//如果u的质量符合要求,把该颜色并入sum【u】
    Min(dp[u][sum[u]],cut[u]);//一刀砍断,只需要一条边的花费,看是否能更新相应的状态,这里把边转换为了点权
}
int get(char s){
    if(s=='R') return 0;
    if(s=='G') return 1;
    if(s=='B') return 2;
    if(s=='I') return 3;
    if(s=='V') return 4;
    if(s=='Y') return 5;
    if(s=='O') return 6;
}
int main()
{
    int t,i,j,k,n,tot,fa;
    char s[10];
    scanf("%d",&t);
    while(t--)
    {
        for(i=0;i<=n;i++) edge[i].clear();
        scanf("%d%d",&n,&tot);
        for(i=1;i<=n;i++)
        {
            scanf("%s%d%d%d",s,&qual[i],&fa,&cut[i]);
            col[i]=get(s[0]);
            edge[fa].push_back(i);
            edge[i].push_back(fa);
        }cut[0]=inf;
        int l=1,r=1000,mid;
        int best=-1;
        while(l<=r)
        {
            mid=(l+r)>>1;
            dfs(0,-1,mid);
            if(dp[0][(1<<7)-1]<=tot) 
            {
                best=mid;
                l=mid+1;
            }
            else r=mid-1;
        }
        if(best==-1) printf("Stay hungry\n");
        else printf("%d\n",best);
    }
    return 0;
}

 

 zoj 3527 带环的树形DP

View Code
/*
很好的树形DP,破环为树;
这道题目很特殊,看到的时候根本无从下手,因为以前没接触过这种类型的树形DP,网上搜了一下发现自己简直弱爆了。。
这是一类最简单的树形DP,考虑某个节点选或不选,然后把两种状态从叶子向根更新上去即可,
但是这道题中给你的不是一棵树,是图,特殊的图
每个点都只有一个父亲(或一个儿子),所以形成的环是简单环,所以可以考虑破环,
从环上的一个点出发开始DP,这个点的两种状态(选或不选)分别枚举一下
*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long lld;
const lld inf = (lld)1<<50;
const int M = 100010;
int w[M];
int n;
int head[M],nxt[M],pnt[M],wi[M],p[M];
int E;
lld dp[M][2];
void add(int a,int b,int w)
{
    pnt[E]=b;
    wi[E]=w;
    nxt[E]=head[a];
    head[a]=E++;
}
void dfs(int u,int F)
{
    dp[u][0]=0;
    dp[u][1]=w[u];
    for(int i=head[u];i!=-1;i=nxt[i])
    {
        if(pnt[i]!=F)
            dfs(pnt[i],F);
        dp[u][0]+=max(dp[pnt[i]][0],dp[pnt[i]][1]);
        dp[u][1]+=max(dp[pnt[i]][0],dp[pnt[i]][1]+wi[i]);
    }
}
lld solve(int u)
{
    dp[u][0]=0;
    dp[u][1]=-inf;//不选u,这个状态不合法
    lld ans1=0,ans2=0;
    for(int i=head[u];i!=-1;i=nxt[i])
    {
        dfs(pnt[i],u);
        ans1+=max(dp[pnt[i]][0],dp[pnt[i]][1]);
    }
    dp[u][0]=-inf;//选u,这个状态不合法
    dp[u][1]=w[u];
    for(int i=head[u];i!=-1;i=nxt[i])
    {
        dfs(pnt[i],u);
        ans2+=max(dp[pnt[i]][0],dp[pnt[i]][1]+wi[i]);
    }
    return max(ans1,ans2);
}
int vis[M];
int main()
{
    while(scanf("%d",&n)!=EOF)
    {
        E=0;
        fill(head,head+n+1,-1);
        fill(p,p+n+1,-1);
        for(int i=1,j;i<=n;i++)
        {
            scanf("%d%d%d",&w[i],&j,&p[i]);
            add(p[i],i,j);
        }
        fill(vis,vis+n+1,-1);
        lld ans=0;
        for(int i=1,j;i<=n;i++)
        {
             if(vis[i]!=-1) continue;
             for(j=i;vis[j]==-1;j=p[j])
                 vis[j]=i;
             if(vis[j]==i) ans+=solve(j);
        }
        printf("%lld\n",ans);
    }
    return 0;
}

 

 

 

CH341A编程器是一款广泛应用的通用编程设备,尤其在电子工程和嵌入式系统开发领域中,它被用来烧录各种类型的微控制器、存储器和其他IC芯片。这款编程器的最新版本为1.3,它的一个显著特点是增加了对25Q256等32M芯片的支持。 25Q256是一种串行EEPROM(电可擦可编程只读存储器)芯片,通常用于存储程序代码、配置数据或其他非易失性信息。32M在这里指的是存储容量,即该芯片可以存储32兆位(Mbit)的数据,换算成字节数就是4MB。这种大容量的存储器在许多嵌入式系统中都有应用,例如汽车电子、工业控制、消费电子设备等。 CH341A编程器的1.3版更新,意味着它可以更多的芯片型号兼容,特别是针对32M容量的芯片进行了优化,提高了编程效率和稳定性。26系列芯片通常指的是Microchip公司的25系列SPI(串行外围接口)EEPROM产品线,这些芯片广泛应用于各种需要小体积、低功耗和非易失性存储的应用场景。 全功能版的CH341A编程器不仅支持25Q256,还支持其他大容量芯片,这意味着它具有广泛的兼容性,能够满足不同项目的需求。这包括但不限于微控制器、EPROM、EEPROM、闪存、逻辑门电路等多种类型芯片的编程。 使用CH341A编程器进行编程操作时,首先需要将设备通过USB连接到计算机,然后安装相应的驱动程序和编程软件。在本例中,压缩包中的"CH341A_1.30"很可能是编程软件的安装程序。安装后,用户可以通过软件界面选择需要编程的芯片类型,加载待烧录的固件或数据,然后执行编程操作。编程过程中需要注意的是,确保正确设置芯片的电压、时钟频率等参数,以防止损坏芯片。 CH341A编程器1.3版是面向电子爱好者和专业工程师的一款实用工具,其强大的兼容性和易用性使其在众多编程器中脱颖而出。对于需要处理25Q256等32M芯片的项目,或者26系列芯片的编程工作,CH341A编程器是理想的选择。通过持续的软件更新和升级,它保持了现代电子技术同步,确保用户能方便地对各种芯片进行编程和调试。
### 链剖分动态规划的结合用法 链剖分是一种用于处理上路径查询和修改的技术,通过两次深度优先搜索 (DFS) 将转化为线性区间,并利用数据结构(如线段状数组)加速操作。动态规划则可以通过预处理子问题的答案来减少冗余计算。两者的结合主要体现在对上的某些属性进行快速更新和查询时,使用动态规划的思想优化状态转移。 以下是具体的实现方法以及代码示例: #### 实现原理 1. **链剖分的核心** 使用两次 DFS 完成轻重边划分,将划分为若干条重链。每条重链对应一个连续的编号范围,便于后续在线段或其他数据结构中进行区间操作[^4]。 2. **动态规划的状态定义** 假设我们需要维护某种节点之间的关系(例如最大值、最小值或总和),可以定义 DP 状态 `dp[u]` 表示以节点 `u` 为根的子中的某个最优解。对于不同类型的题目DP 的具体含义会有所不同。 3. **状态转移方程** 利用链剖分后的父子关系,可以从父节点向子节点传递信息,或者反过来从子节点向上汇总到父节点。这种自底向上的方式非常适合动态规划的应用场景。 4. **结合线段/状数组** 如果需要频繁地对某条路径上的数值进行修改或查询,则可以在链剖分的基础上引入线段等辅助工具,进一步提升效率。 --- #### 代码示例:链剖分 + 动态规划 下面是一个典型的例子——在上求路径的最大权值和。我们将结合链剖分和动态规划完成这一目标。 ```python from collections import defaultdict class TreeChainDecomposition: def __init__(self, n): self.n = n self.graph = [[] for _ in range(n)] self.parent = [-1] * n self.depth = [0] * n self.size = [0] * n self.heavy = [-1] * n self.chain_idx = [0] * n self.pos_in_base = [0] * n self.base_array = [] def add_edge(self, u, v): self.graph[u].append(v) self.graph[v].append(u) def first_dfs(self, root=0): # 计算 size 和 heavy 子节点 stack = [(root, -1)] while stack: node, prev_node = stack.pop() if prev_node != -1 and self.parent[node] == -1: self.parent[node] = prev_node self.depth[node] = self.depth[prev_node] + 1 sub_size = 0 max_subtree = -1 for child in self.graph[node]: if child != prev_node: stack.append((child, node)) sub_size += 1 if max_subtree == -1 or self.size[child] > self.size[max_subtree]: max_subtree = child self.size[node] = sub_size + 1 self.heavy[node] = max_subtree def second_dfs(self, root=0, chain_root=-1): # 给定重链索引并分配 base 数组位置 pos = len(self.base_array) self.chain_idx[root] = chain_root if chain_root != -1 else root self.pos_in_base[root] = pos self.base_array.append(root) if self.heavy[root] != -1: # 处理重儿子 self.second_dfs(self.heavy[root], self.chain_idx[root]) for child in self.graph[root]: # 非重儿子单独形成新链 if child != self.parent[root] and child != self.heavy[root]: self.second_dfs(child, child) def update_segment_tree(tree, idx, value, start, end, seg_pos=1): if start == end: tree[seg_pos] = value return mid = (start + end) // 2 if idx <= mid: update_segment_tree(tree, idx, value, start, mid, seg_pos*2) else: update_segment_tree(tree, idx, value, mid+1, end, seg_pos*2+1) tree[seg_pos] = max(tree[seg_pos*2], tree[seg_pos*2+1]) def query_segment_tree(tree, l, r, start, end, seg_pos=1): if l > end or r < start: return float('-inf') if l <= start and end <= r: return tree[seg_pos] mid = (start + end) // 2 left_query = query_segment_tree(tree, l, r, start, mid, seg_pos*2) right_query = query_segment_tree(tree, l, r, mid+1, end, seg_pos*2+1) return max(left_query, right_query) # 初始化和输入样例 n = 8 edges = [[0, 1], [0, 2], [1, 3], [1, 4], [2, 5], [2, 6], [6, 7]] weights = {i: i+1 for i in range(n)} # 节点权重 tcd = TreeChainDecomposition(n) for u, v in edges: tcd.add_edge(u, v) tcd.first_dfs() # 第一次 DFS 构建大小和重儿子信息 tcd.second_dfs() # 第二次 DFS 进行链划分 segment_tree = [float('-inf')] * (len(tcd.base_array)*4) # 创建线段 for i in range(len(tcd.base_array)): update_segment_tree(segment_tree, i, weights[tcd.base_array[i]], 0, len(tcd.base_array)-1) # 查询路径最大值函数 def query_path_max(node_u, node_v): result = float('-inf') while tcd.chain_idx[node_u] != tcd.chain_idx[node_v]: if tcd.depth[tcd.chain_idx[node_u]] < tcd.depth[tcd.chain_idx[node_v]]: node_u, node_v = node_v, node_u current_chain_head = tcd.chain_idx[node_u] pos_start = tcd.pos_in_base[current_chain_head] pos_end = tcd.pos_in_base[node_u] result = max(result, query_segment_tree(segment_tree, pos_start, pos_end, 0, len(tcd.base_array)-1)) node_u = tcd.parent[current_chain_head] last_chain_head = tcd.chain_idx[node_u] pos_start = min(tcd.pos_in_base[node_u], tcd.pos_in_base[node_v]) pos_end = max(tcd.pos_in_base[node_u], tcd.pos_in_base[node_v]) result = max(result, query_segment_tree(segment_tree, pos_start, pos_end, 0, len(tcd.base_array)-1)) return result # 测试查询 print(query_path_max(0, 7)) # 输出路径最大值 ``` 上述代码展示了如何结合链剖分和动态规划解决问题的过程。其中,动态规划的部分体现在对每个节点的最终权值进行了预先存储,并通过线段支持高效的区间查询[^5]。 --- ### 总结 链剖分提供了强大的工具来简化树形结构的操作,而动态规划能够有效降低复杂度。两者结合后,在许多实际应用中表现出色,尤其是在涉及大量路径查询的情况下。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值