1. 程式人生 > 實用技巧 >ACM 樹上揹包DP總結

ACM 樹上揹包DP總結

題目一

CCF 201909-5 城市規劃(http://118.190.20.162/view.page?gpid=T90)

題目大意


思路

\(f[u][i]\)表示在u這棵子樹(含u)裡選了i個點的最小代價。
考慮轉移:

假設這棵子樹選擇了q個,那麼這條邊經過了(k-q)*q次。注意的是:揹包的DP必須從大到小或者用輔助陣列。

#include <bits/stdc++.h>
#define LL long long
const LL INF=0x3f3f3f3f3f3f3f3fll;
using namespace std;
struct node{
    int to, w;
};

vector<node> v[50005];
int s[50005]={0};
LL num[50005]={0};
LL f[50005][105]={0}, t[105];
int n, m, k;
void dfs(int u, int fa){

    f[u][0]=0;
    if(s[u]==1){
        f[u][1]=0;
        num[u]=1;
    }
    for(int r=0; r<v[u].size(); r++){
        int to=v[u][r].to, w=v[u][r].w;
        if(to!=fa){
            dfs(to, u);

            int sum=min(1ll*k ,num[u]+num[to]), mn=min(1ll*k, num[u]);

            for(int i=0; i<=sum; i++){
                t[i]=f[u][i];
            }

            for(int p=0; p<=mn; p++){//其餘子樹i個
                for(int q=0; p+q<=sum; q++){//當前子樹j個
                    t[p+q]=min(t[p+q], f[u][p]+f[to][q]+(q)*(k-q)*1ll*w);
                }
            }

            for(int i=0; i<=sum; i++){
                f[u][i]=min(f[u][i], t[i]);
            }
            num[u]+=num[to];
        }
    }
}

int main()
{
    int u, to, w;
    scanf("%d%d%d", &n, &m, &k);
    for(int i=1; i<=m; i++){
        scanf("%d", &u);
        s[u]=1;
    }
    memset(f, INF, sizeof(f));
    for(int i=1; i<=n-1; i++){
        scanf("%d%d%d", &u, &to, &w);
        v[u].push_back(node{to, w});
        v[to].push_back(node{u, w});
    }
    dfs(1, 0);
    printf("%lld\n", f[1][k]);

    return 0;
}

題目二

Wannafly挑戰賽27 藍魔法師(https://ac.nowcoder.com/acm/problem/20811)

題目描述

“你,你認錯人了。我真的,真的不是食人魔。”--藍魔法師

給出一棵樹,求有多少種刪邊方案,使得刪後的圖每個連通塊大小小於等於k,兩種方案不同當且僅當存在一條邊在一個方案中被刪除,而在另一個方案中未被刪除,答案對998244353取模

輸入描述:

第一行兩個整數n,k, 表示點數和限制
2 <= n <= 2000, 1 <= k <= 2000
接下來n-1行,每行包括兩個整數u,v,表示u,v兩點之間有一條無向邊
保證初始圖聯通且合法

輸出描述:

共一行,一個整數表示方案數對998244353取模的結果
示例1

輸入

5 2
1 2
1 3
2 4
2 5

輸出

7

思路

\(f[u][i]\)表示u的所有子樹滿足條件,並且u在的連通塊的大小為i的方案樹。
考慮轉移:
u和to這棵子樹不連通, 那麼to子樹的所有方案都滿足條件。

如果u和to連通,考慮u這棵子樹選擇了p個,to選擇了q個。

時間複雜度分析

每次合併兩棵子樹 u, v 時,需要花費 siz[u] * siz[v] 的代價,所以總複雜度是 \(\sum siz[u] * siz[v]\)。整體來看,這就是說每個點對只會在 LCA 處,對複雜度有 1 的貢獻,所以複雜度是點對數,也就是 O(n^2)。

#include<bits/stdc++.h>
#define LL long long
using namespace std;

const LL mod=998244353;
LL f[2005][2005], num[2005];
LL T[2005], siz[2005];
LL k;

struct Edge{
    int to, nxt;
}e[5005];
int cut=0, head[2005];
void addedge(int u, int v){
    e[++cut]={v, head[u]};
    head[u]=cut;
}

void DFS(int u, int fa) {
    f[u][1]=siz[u]=1;
    for(int i=head[u]; i; i=e[i].nxt) {
        int x=e[i].to;
        if(x==fa) continue;
        DFS(x, u);


        for(int i=1; i<=k; i++) {
            T[i]=0;
        }

        int n=min(siz[u], k), m=min(k, siz[u]+siz[x]);
        for (int p = 1; p <= n; p++) { // 列舉已經處理了的包含根節點的子樹對新連通塊的貢獻;
            for (int q = 0; q <= siz[x] && p + q <= m; q++) // 當前準備處理的子樹對新連通塊的貢獻;
                // 新聯通塊的大小為 p + q,貢獻的方案數為 f[u][p] * f[to][q];
                T[p + q] = (T[p + q] % mod + f[u][p] * f[x][q] % mod) % mod;
        }

        for(int i=1; i<=k; i++) {
            f[u][i]=T[i];// 用T暫存再賦值回去;
        }
        siz[u]+=siz[x];// 更新已經處理了的子樹的大小;

    }

    for(int i=1; i<=k; i++) {// 將以u為根的所有的可行方案數存在dp[u][0]裡;
        f[u][0]+=f[u][i];
        f[u][0]%=mod;
    }
}

int main() {
    int n, x, y;
    scanf("%d%lld", &n, &k);
    for(int i=1; i<n; i++) {
        scanf("%d%d", &x, &y);
        addedge(x, y);
        addedge(y, x);
    }
    DFS(1, 0);
    printf("%lld\n", f[1][0]);

    return 0;
}