题集

比较有代表性的一些题。

树上背包

P1273 有线电视网

给你一棵树,1 为根,叶子节点有一个价值 \(v\),边有边权 \(c\),求使在总费用 \(\sum c\le \sum v\) 的前提下,可以选择叶子的最大数量。

\(u\) 子树内叶子个数为 \(cnt_u\)

选数量,大概率是背包了。选用分组背包,原因是考虑到一个点,它可以选择 \(0\sim cnt_u\) 个叶子,视 \(A_u=\{选0个, 选1个,...,选cnt_u个\}\) 为一个组,显然组内元素只能选择一个,既选 1 个又选 3 个相当于选 4 个。

  • 分组背包实质上就是对于每个组跑一遍 01 背包。

设计状态,设 \(f_{u,i,j}\) 为在 \(u\) 子树中,遍历了前 \(i\) 个叶子所在子树,选择了 \(j\) 个叶子的最大利润。

枚举 \(k\) 表示在儿子 \(v\) 中选择了 \(k\) 个叶子,则有

\[f_{u,i,j}=\max\limits_{k=0}^{cnt_v}(f_{u,i-1,j},f_{u,i-1,j-k}+f_{v,i-1,k}-w) \]

01 背包能滚掉一维,滚掉 \(i\) 这一维,记得要倒序枚举背包容量(等于已经遍历到的子树大小,可以将 \(cnt_u\) 和 DP 同时处理)

\[f_{u,j}=\max\limits_{k=0}^{cnt_v}(f_{u,j},f_{u,j-k}+f_{v,k}-w) \]

初始化 \(f_{i\in[1,n],0}=0,f_{i\in[n-m,n],1}=v_i\),什么都不选的利润显然是 0,叶子节点的利润显然是节点权值。其余为 \(-\infty\),因为利润可能为负数。

对于答案的处理,方法很巧妙。

我们考虑从大到小枚举选择叶子的个数 \(j\),则若 \(f_{1,j}\ge 0\) 则说明可以选择 \(j\) 个叶子,这一定是最优的;否则继续枚举。

点击查看代码
#include <bits/stdc++.h>
#define int long long
const int maxn = 3e3 + 3;
const int mod  = 1e9 + 7;
using namespace std;
int n,m,v[maxn],f[maxn][maxn],cnt[maxn];
struct edge{
    int v,w;
    edge(int v=0,int w=0): v(v),w(w){}
};
vector<edge>e[maxn];
void dfs1(int u){
    if(u>n-m){
        f[u][1]=v[u];
        cnt[u]=1;
        return;
    }
    for(edge v:e[u]){
        dfs1(v.v);
        cnt[u]+=cnt[v.v];
        for(int j=cnt[u];~j;j--)
            for(int k=0;k<=cnt[v.v];k++)
                if(j-k>=0)
                    f[u][j]=max(f[u][j],f[u][j-k]+f[v.v][k]-v.w);
    }
}
signed main()
{
    memset(f,-0x3f,sizeof f);
    cin>>n>>m;
    for(int i=1,v,w,k;i<=n-m;i++){
        f[i][0]=0;
        cin>>k;
        for(int j=1;j<=k;j++){
            cin>>v>>w;
            e[i].emplace_back(edge(v,w));
        }
    }
    for(int i=1;i<=m;i++) f[i+n-m][0]=0,cin>>v[i+n-m];
    dfs1(1);
    for(int i=m;i;i--){
        if(f[1][i]>=0){
            cout<<i;
            return 0;
        }
    }
    return 0;
}

分组背包(二进制分组优化)

换根 DP

P3047 [USACO12FEB] Nearby Cows G

给你一个树,有点权 \(c\),求对于每个点,距离 \(\le k\) 的点的 \(\sum c\)

\(n\le 10^5,k\le 20\)

看得出来是树形 DP,对于每个点,未指定根 提示了我们可能是换根 DP。

换根 DP 一般有两步,先钦定起始节点(一般为 1)并以 1 为根预处理,再进行 \(f_v\leftarrow f_u\) 的换根操作。

看到数据范围,大概率是 \(O(nk)\) 做法,开始没看到以为会 T,卡了好久。

画图是个好方法。可以方便地找到规律。

\(g_{u,k}\) 为以 \(u\) 子树内,深度 \(\le k\) 的点权和,\(f_{u,k}\) 为距离 \(u\le k\) 的点权和,设 \(v\in son_u\)\(h_{u,k}\)\(u\) 子树内,深度为 \(k\) 的点权和,则由图可以得到:

\[h_{u,k}=h_{u,k-1}+h_{v,k-1} \]

\[g_{u,k}=h的前缀和 \]

\[f_{v,k}=f_{u,k-1}+g_{u,k}-g_{u,k-2} \]

时间复杂度 \(O(nk)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=1e5+3;
int n,k,f[maxn][23],c[maxn],g[maxn][23],F[maxn];
vector<int>e[maxn];
void dfs(int u,int fa){
    F[u]=fa;
    for(int v:e[u]){
        if(v!=fa){
            dfs(v,u);
            for(int i=1;i<=k;i++){
                g[u][i]+=g[v][i-1];
            }
        }
    }
}
void df(int u,int fa){
    for(int v:e[u]){
        if(v!=fa){
            for(int i=1;i<=k;i++){
                f[v][i]=f[u][i-1]+g[v][i];
                if(i>1) f[v][i]-=g[v][i-2];
            }
            df(v,u);
        }
    }
}
signed main(){
    ios::sync_with_stdio(0);
    cin>>n>>k;
    for(int i=1,u,v;i<n;i++){
        cin>>u>>v;
        e[u].emplace_back(v);
        e[v].emplace_back(u);
    }
    for(int i=1;i<=n;i++){
        cin>>c[i];
        f[i][0]=g[i][0]=c[i];
    }
    dfs(1,0);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=k;j++){
            g[i][j]+=g[i][j-1];
        }
    }
    for(int i=0;i<=k;i++){
        f[1][i]=g[1][i];
    }
    df(1,0);
    for(int i=1;i<=n;i++){
        cout<<f[i][k]<<'\n';
    }
    return 0;
}

需要数学证明的简单树形 DP

P4395 [BOI2003] Gem 气垫车

给你一个树,对每个点赋权,保证相邻节点权值不相等,求权值和最小值。

一开始想填 1, 2 就行,但是瞄了一眼讨论发现是假的。

老实 DP,还是比较简单的,设 \(f_{u,k}\) 表示 \(c_u=k\) 时,子树内的权值和的最小值。则

\[f_{u,k}=\min\limits_{i\not=k}{f_{v,i}}+k \]

时间复杂度是 \(O(nk^2)\) 不可接受,但是可以证明 \(k\)\(\log n\) 级别的,时间复杂度 \(O(n\log^2 n)\)

证明:见 CNCAGN 博客

点击查看代码
#include<bits/stdc++.h>
#define int long long
const int maxn=1e4+3;
const int logn=20;
using namespace std;
int n,f[maxn][logn+3];
vector<int>e[maxn];
void dfs(int u,int fa){
    for(int i=1;i<=logn;i++){
        f[u][i]=i;
    }
    for(int v:e[u]){
        if(v!=fa){
            dfs(v,u);
            for(int i=1;i<=logn;i++){
                int mi=maxn*(logn+3);
                for(int j=1;j<=logn;j++){
                    if(i==j) continue;
                    mi=min(mi,f[v][j]);
                }
                f[u][i]+=mi;
            }
        }
    }

}
signed main(){
    cin>>n;
    for(int i=1,u,v;i<n;i++){
        cin>>u>>v;
        e[u].emplace_back(v);
        e[v].emplace_back(u);
    }
    dfs(1,0);
    int ans=maxn*(logn+3);
    for(int i=1;i<=logn;i++) ans=min(ans,f[1][i]);
    cout<<ans;
    return 0;
}

刷表法 DP

应该是最经典的 DP 之一。

P3558 [POI2013] BAJ-Bytecomputer

给定一个长度为 \(n\) 的只包含 \(-1,0,1\) 的数列 \(a\),每次操作可以使 \(a_i\gets a_i+a_{i-1}\),求最少操作次数使得序列单调不降。如果不可能通过该操作使得序列单调不降,请输出 BRAK

数据范围:\(1\le n\le 10^6\)

\(f_{i,-1/0/1}\) 表示 \(i\) 位为 \(-1/0/1\) 时的最小操作次数,\(i=1\) 时。接下来分类讨论:

  • \(a_i=-1\)
    \(f_{i,-1}=f_{i-1,-1}\),当前位为 \(-1\) 时,只能从 \(-1\) 转移,且不需要任何操作;
    当前一位为 \(1\) 时,才能将当前位的值提高,所以要把当前位的 \(-1\) 变成 \(0/1\) 都需要消耗操作次数,则有转移:\(f_{i,0}=\begin{cases}\min(f_{i-1,-1},f_{i-1,0})+1&a_{i-1}=1\\ +\infty&\text{otherwise}\end{cases}\)
    其他情况差不多,就看代码罢。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using 
namespace 
std;
const int maxn=1e6+3;
const int inf=0x3f3f3f3f;
int f[maxn][3],n;
int a[maxn];
signed main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    f[1][0]=f[1][1]=f[1][2]=inf;
    f[1][a[1]+1]=0;
    for(int i=2;i<=n;i++){
        if(a[i]==-1){
            f[i][0]=f[i-1][0];
            if(a[i-1]==1) f[i][1]=min(f[i-1][1],f[i-1][0])+1;
            else f[i][1]=inf;
            if(a[i-1]==1) f[i][2]=min(min(f[i-1][2],f[i-1][1]),f[i-1][0])+2;
            else f[i][2]=f[i-1][2]+2;
        }else if(a[i]==0){
            f[i][0]=f[i-1][0]+1;
            f[i][1]=min(f[i-1][1],f[i-1][0]);
            if(a[i-1]==1) f[i][2]=min(min(f[i-1][2],f[i-1][1]),f[i-1][0])+1;
            else f[i][2]=f[i-1][2]+1;
        }else{
            f[i][0]=f[i-1][0]+2;
            if(a[i-1]==-1) f[i][1]=min(f[i-1][1],f[i-1][0])+1;
            else f[i][1]=f[i][0]+1;
            f[i][2]=min(min(f[i-1][2],f[i-1][1]),f[i-1][0]);
        }
    }
    int t=min(f[n][0],min(f[n][1],f[n][2]));
    if(t==inf) cout<<"BRAK";
    else cout<<t;
    return 0;
}
posted @ 2024-06-29 16:51  view3937  阅读(15)  评论(0编辑  收藏  举报
Title