• 多叉树的背包问题


    明天开学

    问题模型一般是:

    给定n个物品,有一个容积为V的背包,每个物品依赖一个物品,选择一个必须选择所依赖的物品,依赖关系构成一棵树,每个物品有体积,价值,最大化价值;

    这种模型称作树形有依赖背包问题;

    常见方法大概有三种:

    1.多叉树转化为二叉树:

    将一个节点的多个儿子中选择最左的儿子保留下来,剩下的儿子放到左儿子的右子树里;

    来张图片:

    状态转移即为:

    设dp[i][j]表示在以i为根的子树中,用大小为j的包能取得的最大价值,

    dp[i][j]=max(dp[left[i]][j-w[i]]+v[i],dp[right[i]][j]);

    left[i]是i在原树中的第一个儿子,right[i]是i在原树中的下一个兄弟。

    第二种方法:dfs序表示法:

    我们对于每一个节点求出他在原树中的dfs序dfn值,然后记录他的子数大小size;

    那么对于一个节点x:x的第一个儿子ydfn[y]=dfn[x]+1;

    对于x的第二个儿子z,dfn[z]=dfn[x]+size[y]+1;

    对于x的一个兄弟k,有dfn[k]=dfc[x]+size[x]+1;

    一个dfs序为i的节点u,同样设dp[i][j]表示在以u为根的子树中,用大小为j的包能取得的最大价值,

    dp[i][j]+w[i]->dp[i+1][j-v[i]]

    dp[i][j]->dp[i+size[i]+1][j]

    这也是较常用的刷表法,用当前状态更新后继的状态;

    两种方法主要解决点权问题;

    那么我们看一下用树形分组背包问题:

    一道经典问题 选课:题目传送门;

    题解(https://www.cnblogs.com/Tyouchie/p/10830072.html);

    我们设dp[k][i][j]表示以i为根的子树,在前k个儿子中,选出一个大小为j的子树(必须包含i),所获得的最大学分。

    那么我们每计算到第k+1个新的儿子v时(size[v]表示v的儿子个数),

    dp[k+1][i][j]=min(dp[k][i][j-t]+dp[size[v]][v][t]);

    k这一维显然可以无用的;

    那么我们改为f[i][j]表示在以i为根的子树内选择了j门课程所获得的最大学分;

    dp[i][j]=max(dp[i][j-t]+dp[v][t]);

    j=m-1->1(m指容积,-1是因为当前点占据了1的体积);

    t=1->j(在我那片题解中我倒序枚举了t,但是我发现两者都是正确的);

    这个模型感觉要理解并且掌握,因为很多树形背包类dp的状态都是这样转移的;

    #include<bits/stdc++.h>
    using namespace std;
    int lin[1000],tot,n,m,x,f[400][400],s[400];
    template<typename T>inline void read(T &x)
    {
        x=0;T f=1,ch=getchar();
        while(!isdigit(ch))  {if(ch=='-')  f=-1;  ch=getchar();}
        while(isdigit(ch))  {x=x*10+ch-'0';  ch=getchar();}
        x*=f;
    }
    struct gg {
        int y,next;
    }a[2000];
    inline void add(int x,int y) {
        a[++tot].y=y;
        a[tot].next=lin[x];
        lin[x]=tot;
    } 
    inline void dp(int x) {
        f[x][0]=0;
        for(int i=lin[x];i;i=a[i].next) {
            int y=a[i].y;
            dp(y);
            for(int t=m;t>=0;--t) {
                for(int j=0;j<=t;++j) {
                    if(t-j>=0) {
                        f[x][t]=max(f[x][t],f[x][t-j]+f[y][j]);
                    }
                }
            }
        }
        if(x!=0) {
            for(int t=m;t>0;t--) 
                f[x][t]=f[x][t-1]+s[x];
        }
    }
    int main() {
        read(n);read(m);
        for(int i=1;i<=n;i++) {
            read(x);
            add(x,i);
            read(s[i]);
        }
        dp(0);
        cout<<f[0][m]<<endl;
        return 0;
    }
    View Code

    重建道路;

    f[i][j]表示以i为根的子树内分离一个大小为j的子树所需要的最少操作数;

    j=p->1;

    t=1->j;

    和上一道题目很像;

    显然f[x][1]为每个点x的度;

    再转移的时候-2是因为u->v的边和v->u的边都已经在初始化的时候被减掉了,所以这时候要把他们连起来就得减去他们两个造成的贡献,也就是2.,思考一下,很好理解;

    #include<bits/stdc++.h>
    using namespace std;
    template<typename T>inline void read(T &x) {
        x=0;
        T f=1,ch=getchar();
        while (!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();}
        while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
        x*=f;
    }
    const int N=250; 
    struct gg {
        int y,next,v;
    }a[N<<1];
    int n,p,tot,x,y,lin[N],du[250],dp[250][250];
    inline void add(int x,int y) {
        a[++tot].y=y;
        a[tot].next=lin[x];
        lin[x]=tot;
    }
    inline void dfs(int x,int fa) {
        dp[x][1]=du[x];
        for(int i=lin[x];i;i=a[i].next) {
            int y=a[i].y;
            if(y==fa) continue;
            dfs(y,x);
            for(int j=p;j>=1;j--) {
                for(int t=1;t<=j;t++) {
                    dp[x][j]=min(dp[x][j],dp[x][j-t]+dp[y][t]-2);
                }
            }
        }
    }
    int main() {
        read(n); read(p);
        for(int i=1;i<n;i++) {
            read(x); read(y);
            add(x,y);
            add(y,x);
            du[x]++; du[y]++;
        }
        memset(dp,0x3f,sizeof(dp));
        dfs(1,0);
        int ans=1<<30;
        for(int i=1;i<=n;i++) {
            ans=min(ans,dp[i][p]);
        }
        cout<<ans<<endl;
        return 0;
    }
    View Code

    有线电视网

    真正理解前两到题目这道也就不难了;

    我们设f[i][j]表示在以i为根的子树内,让j个人看所获得的最大收益;

    那么对于每一个叶子节点x,f[x][1]就等于val[x];

    那么最后我们寻找一个最大的i满足f[1][i]>=0;

    所以我们倒序扫描就行了;

    状态转移是j,t的枚举和前两题是一样的;

    根据当前背包容积转移;

    #include<bits/stdc++.h>
    using namespace std;
    template<typename T>inline void read(T &x) {
        x=0;
        T f=1,ch=getchar();
        while (!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();}
        while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar();
        x*=f;
    }
    int n,m,dp[3010][3010],val[3010],k,x,v,tot,lin[3010];
    struct gg {
        int y,next,v;
    }a[1000100];
    inline void add(int x,int y,int v) {
        a[++tot].y=y;
        a[tot].next=lin[x];
        lin[x]=tot;
        a[tot].v=v;
    }
    int dfs(int u)
    {
        if(u>n-m) {
            dp[u][1]=val[u];
            return 1;
        }  
        int sum=0,s,t;
        for(int i=lin[u];i;i=a[i].next) {
            int y=a[i].y;
            s=dfs(y);
            sum+=s;
            for(int j=sum;j>0;j--) {
                for(int t=1;t<=j;t++) {
                        dp[u][j]=max(dp[u][j],dp[u][j-t]+dp[y][t]-a[i].v);
                }
            }
        }
        return sum;
    }
    int main() {
        memset(dp,0xcf,sizeof(dp));
        read(n); read(m);
        for(int i=1;i<=n-m;i++) {
            read(k);
            for(int j=1;j<=k;j++) {
                read(x); read(v);
                add(i,x,v);
            }
        }
        for(int i=n-m+1;i<=n;i++) read(val[i]);
        for(int i=1;i<=n;i++) dp[i][0]=0;
        dfs(1);
        for(int i=m;i>=1;i--)
            if(dp[1][i]>=0) {
                printf("%d",i);
                break;
            }
        return 0;
    }
    View Code
  • 相关阅读:
    C++快速排序
    C++冒泡排序
    为什么Excel创建一个新的工作簿就会初始化三个worksheet
    为什么游戏需要英雄
    2015.11.18——Lua中文教程
    [国家集训队2012]JZPFAR
    后缀数组小结?
    [BZOJ 2738]矩阵乘法
    [BZOJ 3221][Codechef FEB13] Obserbing the tree树上询问
    [BZOJ 4999]This Problem Is Too Simple!
  • 原文地址:https://www.cnblogs.com/Tyouchie/p/11395102.html
Copyright © 2020-2023  润新知