【集训】树上 & 状压DP

P1352 没有上司的舞会

树形 DP

fi,0/1 表示考虑 i 的子树,不选 i 和 选 i 的最大值。

转移:fi,1=maxvson(u)fv,0fi,0=maxvson(u)max(fv,0,fv,1)

DFS一遍求解即可。

#include <bits/stdc++.h>
using namespace std;
int dp[100010][2],a[100010];
vector<int> G[100010];
void dfs(int rt,int fa){
	dp[rt][0]=0;
	dp[rt][1]=a[rt];
	for(int i=0;i<G[rt].size();i++){
		int to=G[rt][i];
		if(to==fa) continue;
		dfs(to,rt);
		dp[rt][1]=dp[rt][1]+max(0,dp[to][0]);
		dp[rt][0]=dp[rt][0]+max(0,max(dp[to][0],dp[to][1]));
	}
}
int main(){
	int n,u,v;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n-1;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	dfs(1,0);
	int ans=max(ans,max(dp[1][0],dp[1][1]));
	cout<<ans<<endl;
 	return 0;
}

P2014 [CTSC1997] 选课

树形 DP

fi,j 表示以 i 为根的子树,选了 j 门课程的最大学分

初值:fu,1=su

转移:从下往上dp,fu,j=maxk=1min(j1,sizev)fu,jk+fv,k

因为 j>k 才能转移,即不能从 fu,0 转移过来,因为不能不选根节点。

答案: f0,m+1

时间复杂度 O(n2)

#include<bits/stdc++.h>
using namespace std;
const int N=305,M=2001;
int f[N][N],siz=0,n,m;
int head[M],nxt[M],son[M];
void add(int x,int y){
    siz++;
    nxt[siz]=head[x];
    head[x]=siz;
    son[siz]=y;
}
void dp(int x){
    for (int e=head[x];e;e=nxt[e]){
        int y=son[e];
        dp(y);
        for (int i=m+1;i>1;i--)
            for (int j=i-1;j>0;j--)
                f[x][i]=max(f[x][i],f[x][i-j]+f[y][j]);
    }
}
int main(){
    cin>>n>>m;
    for (int i=1;i<=n;i++){
        int a,tmp;
        cin>>a>>tmp;
        f[i][1]=tmp;
        add(a,i);
    }
    dp(0);
    cout<<f[0][m+1]<<endl;
    return 0;
}

P2607 [ZJOI2008] 骑士

基环树 DP

每个点向自己讨厌的人反向连边,形成基环内向树。
找到环,枚举环上的每一个点,并断开,于是转化成了 没有上司的舞会。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;
const int N = 1000010, INF = 1e9;

int n;
int h[N], e[N], rm[N], w[N], ne[N], idx;
LL f1[N][2], f2[N][2];
bool st[N], ins[N];
LL ans;

inline void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs_f(int u, int ap, LL f[][2]){
    for (int i = h[u]; ~i; i = ne[i]){
        if (rm[i]) continue;
        int j = e[i];
        dfs_f(j, ap, f);
        f[u][0] += max(f[j][0], f[j][1]);
    }

    f[u][1] = -INF;
    if (u != ap){
        f[u][1] = w[u];
        for (int i = h[u]; ~i; i = ne[i]){
            if (rm[i]) continue;
            int j = e[i];
            f[u][1] += f[j][0];
        }
    }
}

void dfs_c(int u, int from){
    st[u] = ins[u] = true;
    for (int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if (!st[j]) dfs_c(j, i);
        else if (ins[j]){
            rm[i] = 1;
            dfs_f(j, -1, f1);
            dfs_f(j, u, f2);
            ans += max(f1[j][0], f2[j][1]);
        }
    }

    ins[u] = false;
}

int main(){
    cin >> n;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i ++ ){
        int a, b;
        cin >> a >> b;
        add(b, i);
        w[i] = a;
    }
    for (int i = 1; i <= n; i ++ )
        if (!st[i])
            dfs_c(i, -1);

    cout << ans << endl;
    return 0;
}

P3177 [HAOI2015] 树上染色

树形 DP

fi,j 表示 i 这个子树选了 j 个黑点的最大值。
直接算答案不好算,考虑把答案拆分到边权上
枚举每一条边,发现对答案的贡献为:(k(mk)+(sz[v]k)(nmsz[v]+k))×e[i].w
DFS 一遍求解即可

#include<bits/stdc++.h>
#define ll long long
const int N=2005;
using namespace std;
struct ahaha{
	int w,to,nxt;
}e[N<<1];int tot,h[N];
inline void add(int u,int v,int w){
	e[tot].w=w,e[tot].to=v,e[tot].nxt=h[u];h[u]=tot++;
}

int n,m,sz[N];
ll f[N][N];
void dfs(int u,int fa){
	sz[u]=1;f[u][0]=f[u][1]=0;
	for(int i=h[u];~i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		sz[u]+=sz[v];
		for(int j=min(m,sz[u]);j>=0;--j){  
			if(f[u][j]!=-1)    
				f[u][j]+=f[v][0]+(ll)sz[v]*(n-m-sz[v])*e[i].w;
			for(int k=min(j,sz[v]);k;--k){
				if(f[u][j-k]==-1)continue;
				ll val=(ll)(k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k))*e[i].w; 
				f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]+val);
			}
		}
	}
}

int main(){
    memset(h,-1,sizeof h);
	cin>>n>>m;
	if(n-m<m)m=n-m;
	for(int i=1;i<n;++i){
	    int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w),add(v,u,w);
	}
	memset(f,-1,sizeof f);
	dfs(1,-1);
    cout<<f[1][m]<<endl;
	return 0;
}

P1272 重建道路

考虑设 fi,j 表示 i 的子树中选出一个大小为 j 的连通块的最小步数
转移:fi,j=minvufu,jk+fv,k1
1 是因为中间那条边不用断了
DFS 求解

P3478 [POI2008] STA-Station

n 很大显然不能一个一个算
考虑假设以 1 为根节点,计算出答案为 ans
那么当根节点为 2 时,答案会变成 anssz[2]+nsz[2]
然后 转移就可以了

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2000005;
int n,d[N],sz[N],f[N];
int h[N],e[N],ne[N],idx;
int mx=-1e9,ans;
void add(int u,int v){
    e[++idx]=v,ne[idx]=h[u],h[u]=idx;
}

void dfs(int u,int fa){
    sz[u]=1,d[u]=d[fa]+1;
    for(int i=h[u];i;i=ne[i]){
        int j=e[i];
        if(j==fa) continue;
        dfs(j,u);
        sz[u]+=sz[j];
    }
}

void dfs2(int u,int fa){
    for(int i=h[u];i;i=ne[i]){
        int j=e[i];
        if(j==fa) continue;
        f[j]=f[u]-sz[j]+n-sz[j];
        dfs2(j,u);
    }
}
signed main(){
    cin>>n;
    for(int i=1;i<=n-1;i++){
        int u,v;
        cin>>u>>v;
        add(u,v),add(v,u);
    }
    dfs(1,0);
    for(int i=1;i<=n;i++) f[1]+=d[i];
    dfs2(1,0);
    for(int i=1;i<=n;i++){
        if(f[i]>mx){
            mx=f[i],ans=i;
        }
    }
    cout<<ans<<endl;
    return 0;
}

P4365 [九省联考 2018] 秘密袭击 coat

P3780 [SDOI2017] 苹果树

P1879 [USACO06NOV] Corn Fields G

状压dp板子
考虑 f_{i,S}$ 表示前 i 行,第 i 的状态为 S 的方案书
转移:fi,S+=fi1,TT 需要满足限制。
然后也可以滚动数组优化。

#include <bits/stdc++.h>
using namespace std;
const int N=13,M=1<<12,P=1e8;
int f[N][M],F[N],g[M],a[N][N];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>a[i][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            F[i]=(F[i]<<1)+a[i][j];
	for(int i=0;i<(1<<m);i++)
		if(!(i&(i>>1))&&!(i&(i<<1))){
			g[i]=1;
			if((i&F[1])==i) f[1][i]=1;
		}
	
	for(int x=2;x<=n;x++)
	for(int j=0;j<(1<<m);j++)
		if(((j&F[x-1])==j)&&g[j])
		    for(int k=0;k<(1<<m);k++)
				if(((k&F[x])==k)&&!(j&k)&&g[k])
				f[x][k]=(f[x][k]+f[x-1][j])%P;
	int ans=0;
	for(int i=0;i<(1<<m);i++)
		ans=(ans+f[n][i])%P;
	cout<<ans<<endl;
    return 0;
}

P1896 [SCOI2005] 互不侵犯

fi,j,k 表示第 i 行,状态为 j ,选了 k 个国王的方案数。
DFS 预处理出所有合法的状态,然后暴力转移即可
答案就是 i=1cntfN,i,K

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

const int M = 10; 
const int C = 2000;

int s[C], g[C], cnt = 0, n, y;
long long f[M][C][100];

void dfs(int h, int m, int d) {
    if (d >= n) {
        s[++cnt] = h;
        g[cnt] = m;
        return;
    }
    dfs(h, m, d + 1);
    dfs(h + (1 << d), m + 1, d + 2);
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> y;
    dfs(0, 0, 0);

    for (int i = 1; i <= cnt; i++) {
        f[1][i][g[i]] = 1;
    }

    for (int i = 2; i <= n; i++) {
        for (int j = 1; j <= cnt; j++) {
            for (int k = 1; k <= cnt; k++) {
                if (s[j] & s[k]) continue;
                if ((s[j] << 1) & s[k]) continue;
                if (s[j] & (s[k] << 1)) continue;
                for (int t = y; t >= g[j]; t--) {
                    f[i][j][t] += f[i - 1][k][t - g[j]];
                }
            }
        }
    }

    long long a = 0;
    for (int i = 1; i <= cnt; i++) {
        a += f[n][i][y];
    }

    cout << a << "\n";
    return 0;
}

P3959 [NOIP2017 提高组] 宝藏

状态DP,下文中i是一个 n 位二进制数,表示每个点是否存在。

状态f[i][j]表示:

  • 集合:所有包含i中所有点,且树的高度等于j的生成树
  • 属性:最小花费
  • 状态计算:枚举i的所有非全集子集S作为前j - 1层的点,剩余点作为第j层的点。
  • 核心: 求出第j层的所有点到S的最短边,将这些边权和乘以j,直接加到f[S][j - 1]上,即可求出f[i][j]。

时间复杂度
包含 k 个元素的集合有 Cnk 个,且每个集合有 2k 个子集,因此总共有 Cnk2k 个子集。k 可以取 0n,则总共有 k=0nCnk2k=(1+2)n=3n,这一步由二项式定理可得。

对于每个子集需要 n2 次计算来算出剩余点到子集中的最短边。

因此总时间复杂度是 O(n23n)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 12, M = 1 << N, INF = 0x3f3f3f3f;

int n, m;
int d[N][N], g[N][M];
int f[M][N];

int main()
{
    scanf("%d%d", &n, &m);

    memset(d, 0x3f, sizeof d);
    for (int i = 0; i < n; i ++ ) d[i][i] = 0;
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        a --, b -- ;
        d[a][b] = d[b][a] = min(d[a][b], c);
    }

    memset(g, 0x3f, sizeof g);
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < 1 << n; j ++ )
            for (int k = 0; k < n; k ++ )
                if (j >> k & 1)
                    g[i][j] = min(g[i][j], d[i][k]);

    memset(f, 0x3f, sizeof f);
    for (int i = 0; i < n; i ++ ) f[1 << i][0] = 0;
    for (int i = 1; i < 1 << n; i ++ )
        for (int j = i - 1 & i; j; j = j - 1 & i)  // 枚举i的子集
        {
            int r = i ^ j, cost = 0;
            for (int k = 0; k < n; k ++ )
                if (j >> k & 1)
                {
                    cost += g[k][r];
                    if (cost >= INF) break;
                }
            if (cost >= INF) continue;
            for (int k = 1; k < n; k ++ )
                f[i][k] = min(f[i][k], f[r][k - 1] + cost * k);
        }

    int res = INF;
    for (int i = 0; i < n; i ++ )
        res = min(res, f[(1 << n) - 1][i]);
    printf("%d\n", res);

    return 0;
}


P8189 [USACO22FEB] Redistributing Gifts G

dl题解

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 19, M = 1 << 18; // N为最大点数,M为状态集合的总数(2^n)
int n, Q, s[N]; // n为点的数量,Q为查询次数,s[i]表示点i可以到达的点的集合(用二进制表示)
ll dp[M][N], g[M], h[M]; // dp[i][j]表示从集合i的最小点出发到达点j的方案数,g[i]表示集合i连成一个置换环的方案数,h[i]表示集合i分成若干个置换环的方案数
char c[N]; // 用于读取查询的字符串
int main(){
    cin >> n; // 输入点的数量n
    // 输入每个点的转移关系,构建s数组
    for (int i = 0; i < n; i++){
        bool flag = 0; // 标记是否已经找到了一个自环(即点i可以转移到自己)
        for (int j = 0; j < n; j++){
            int x;
            cin >> x; // 输入点i的下一个转移点x(从1开始编号,减1后从0开始)
            x--;
            if (!flag)
                s[i] |= (1 << x); // 将点x加入点i可以到达的集合s[i]中
            if (x == i) // 如果x等于i,说明存在自环
                flag = 1;
        }
    }
    // 初始化dp数组,每个单独的点i,从i出发到i的方案数为1
    for (int i = 0; i < n; i++)
        dp[1 << i][i] = 1;
    // 动态规划求解dp数组
    for (int i = 1; i < (1 << n); i++){ // 枚举所有状态集合i(从1到2^n-1)
        int p = __lg(i & (-i)); // 找到集合i中最小的点p(即最低位的1)
        for (int j = p; j < n; j++) // 枚举当前集合i的终点j
            if (dp[i][j]) // 如果从集合i的最小点出发可以到达点j
                for (int k = p + 1; k < n; k++) // 枚举下一个点k
                    if (!(i & (1 << k)) && s[j] & (1 << k)) // 如果点k不在集合i中且点j可以转移到点k
                        dp[i | (1 << k)][k] += dp[i][j]; // 更新dp值,将点k加入集合i并转移到点k
                    
    }
    // 计算每个集合i连成一个置换环的方案数g[i]
    for (int i = 1; i < (1 << n); i++) // 枚举所有状态集合i
        for (int j = 0; j < n; j++){ // 枚举集合i的最后一个点j
            int p = __lg(i & (-i)); // 找到集合i中最小的点p
            if (s[j] & (1 << p)) // 如果点j可以回到集合i的最小点p
                g[i] += dp[i][j]; // 更新g[i],将从点j回到点p的方案数加入g[i]
        }
    // 计算将集合i分成若干个置换环的方案数h[i]
    h[0] = 1; // 空集的方案数为1
    for (int i = 1; i < (1 << n); i++) // 枚举所有状态集合i
        for (int j = i; j; j = (j - 1) & i) // 枚举集合i的所有子集j
            if (j & (i & (-i))) // 如果子集j包含集合i的最小点
                h[i] += g[j] * h[i ^ j]; // 更新h[i],将子集j连成一个置换环的方案数与剩余部分的方案数相乘
    // 处理查询
    cin >> Q; // 输入查询次数Q
    while (Q--){
        cin >> c; // 输入查询的字符串c
        int ans = 0; // 初始化查询结果
        for (int i = 0; i < n; i++)
            if (c[i] == 'H') // 如果字符为'H',表示点i在查询集合中
                ans |= (1 << i); // 将点i加入查询集合
        // 输出查询结果,将查询集合的方案数与剩余集合的方案数相乘
        cout << h[ans] * h[((1 << n) - 1) ^ ans] << "\n";
    }
}

P4037 [JSOI2008] 魔兽地图

P3354 [IOI2005] Riv 河流

posted @   Star_F  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示