树形背包

P2014

在树形 DP 的过程中,我们发现这个 DP 式子很类似背包。我们考虑在遍历子树的过程中把下图绿色部分的 \(dp\) 值和紫色部分的 \(dp\) 值合并到一起。

这样树形背包就写完了。时间复杂度 \(O(nk)\),证明见此:https://blog.csdn.net/lyd_7_29/article/details/79854245。

\(O(nk)\) 的证明繁琐,但是我们要知道它是一个平方算法,为什么呢?因为合并大小为a,b的子树复杂度是a*b,可以看成a子树内任选一点,b子树内任选一点进行匹配,不管怎么合并任意两个点只会在其lca匹配一次,所以是n^2的。

image

#include<bits/stdc++.h>
using namespace std;
#define f(i, a, b) for(int i = a; i <= b; i++)
#define mod9 998244353
#define mod1 1000000007
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
#define endl '\n'
#define cl(i, n) i.clear(), i.resize(n);
vector<int> g[310];
int dp[310][310];
int s[310];
int sz[310];
int n, m;
void dfs(int x) {
    sz[x] = 1;
    f(i, 0, (int)g[x].size() -1) {
        dfs(g[x][i]);
        
        for(int j = sz[x]; j >= 1; j--) {
            for(int k = sz[g[x][i]]; k >= 1; k--) {
                dp[x][j + k] = max(dp[x][j + k], dp[x][j] + dp[g[x][i]][k]);
            }
        }
        sz[x] += sz[g[x][i]];
    }
    return;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
     cin >> n >> m;
    f(i, 1, n) {
        int k; cin >> k;
        g[k].push_back(i);
        cin >> s[i];
        
    }
    memset(dp, 0xcf, sizeof(dp));
    f(i ,0, n) dp[i][1] = s[i];
    dfs(0);
    int ans = 0;
    ans= max(ans, dp[0][m+1]);
    cout << ans << endl;
    return 0;
}
CF1779F

【题意】

给定一棵树,点有点权 \(a_i\)

可以进行 \(\le 2n\) 次操作,每次选定一个节点 \(i\),令其子树权值异或和为 \(x\),将其子树内所有权值都变为 \(x\)

求使得所有点点权都变成 \(0\) 的方案,或者报告无解。

\(n \le 2 \times 10^5, a_i \le 31\)

【分析】

考虑如果一棵树的 size 是奇数,做一次操作不会改变异或和。否则,做一次操作会将子树异或和置 \(0\)

考虑偶子树操作两次之后必定清 \(0\),因此如果 \(n\) 是偶数,一定可以直接操作两次。

否则,考虑需要将大小为奇数的树通过对若干棵偶子树进行操作(并且不能有重复操作的范围)把这个树的异或和变成 \(0\)

也就是说,如果原树异或和为 \(xorsum\),那么要选出若干个偶子树使得其两两不交且异或和为 \(xorsum\)

这个问题可以使用树形背包解决。记 \(dp_{i, j}\) 表示第 \(i\) 个子树内选出若干个偶子树使得其两两不交且异或和为 \(j\) 是否可行,并且记录方案。方案的大小不会太大(最小方案一定 \(\le 6\)

考虑转移过程。由于需要两两不交,要特别小心地转移。有这么些转移方式:

  • 考虑一个子树 \(i\),如果它是偶子树,那么 \(dp_{i, xorsum(i)} = \{i\}\)
  • 其子树 \(j\) 上面已经可以组成的数字,\(dp_{i,k} \leftarrow dp_{j,k}\)
  • 背包过程中已经可以组成的数字 \(dp_{i,k}\),当前子树内可以组成的数字 \(dp_{j,l}\),可以组成 \(dp_{i,(k \oplus l)}\)

首先,枚举子树的过程,第二种转移要一个一个地背上,不能先把第二种转移全背上然后再做第三种,可能会出现子树重复。

其次,第一种转移一定要是最后进行。

第三,也是最容易忽视的一个坑:\(j\)\(0 \sim 31\) 枚举的时候,如果当前子树可以组成 \(j\),它是不能放在后面当 \(k\) 用的。因此需要枚举完毕再依次记录标记。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
//调不出来给我对拍!
struct node {
    vector<int> son;
    int sz;
    int a;//xorsum
    bool ok[32];
    vector<int> pc[32];
    node(){sz=1;f(i,0,31)ok[i]=0;}
}d[200100];
int mov[32];
void dfs(int now){
    for(int i:d[now].son){
        dfs(i);
        d[now].a ^= d[i].a;
        d[now].sz +=d[i].sz;
    }
    for(int i:d[now].son){
        f(j, 0, 31) mov[j]=0;
        f(j,0,31){
            if(d[now].ok[j])continue;
            else {
                f(k,0,31){
                    if(!d[now].ok[k])continue;
                    else {
                        int tar = (k ^ j);
                        if(!d[i].ok[tar])continue;
                        else {
                            mov[j]=1;
                            for(int ppt : d[now].pc[k]) d[now].pc[j].push_back(ppt);
                            for(int ppt : d[i].pc[tar]) d[now].pc[j].push_back(ppt);
                            break;
                        }
                    }
                }
            }
        }
        f(j,0,31){
            if(d[now].ok[j])continue;
            if(d[i].ok[j]){
                d[now].ok[j]=1;
                for(int ppt : d[i].pc[j]) d[now].pc[j].push_back(ppt);
            }
        }
        f(j, 0, 31) if(mov[j]) {d[now].ok[j] = 1;}
    }
    if(d[now].sz %2==0) {
        if(!d[now].ok[d[now].a]) {
            d[now].ok[d[now].a] = 1;
            d[now].pc[d[now].a] = {now};
        }
    }
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL);
    cout.tie(NULL);
    //time_t start = clock();
    //think twice,code once.
    //think once,debug forever.
    int n;cin>>n;
    if(n%2==0){cout<<2<<endl<<1<<" "<<1<<endl;return 0;}
    int xorsum = 0;
    f(i,1,n){
        cin>>d[i].a;
        xorsum ^= d[i].a;
    }
    f(i,2,n){
        int p;cin>>p;
        d[p].son.push_back(i);
    }
    dfs(1);
    if(xorsum == 0) {cout<<2<<endl<<1<<" "<<1<<endl;return 0;}
    else if(!d[1].ok[xorsum]){cout<<-1<<endl;return 0;}
    else{cout<<d[1].pc[xorsum].size()+2<<endl;for(int i:d[1].pc[xorsum])cout<<i<<" ";cout<<1<<" "<<1<<endl;}
    //time_t finish = clock();
    //cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
    return 0;
}
posted @ 2022-09-07 23:30  OIer某罗  阅读(132)  评论(0编辑  收藏  举报