1.11 下午-区间 DP & 树形 DP

前言

勿让将来,辜负曾经

从入门到入土……

正文

知识点

区间 DP 和树形 DP 都是动态规划这个大家族中的一个分支

区间 DP 比较明显,数据范围会给你莫大的提示。而树(甚至可以是生成树,缩点后的树,基环树)上的最值、统计方案的问题(期望),都可以往树形 DP 上靠。

一题一解

T1 石子合并(P1775)

链接

区间 DP 的板中之钣,记得初始化即可

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=305;
int n,a[maxn];
int sum[maxn],dp[maxn][maxn];
inline void init(){
	for(int i=1;i<=n;i++){
		sum[i]=sum[i-1]+a[i];
	}
	memset(dp,0x3f,sizeof(dp));
	for(int i=1;i<=n;i++){
		dp[i][i]=0;
	}
	return;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	init();
	for(int len=2;len<=n;len++){
		for(int l=1;l<=n-len+1;l++){
			int r=l+len-1;
			int w=sum[r]-sum[l-1];
			for(int k=l;k<r;k++){
				dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+w);
			}
		}
	}
	cout<<dp[1][n]<<endl;
	return 0;
}

T2 合并珠子(P1063)

链接

还是很套路的区间 DP,需要注意到其特殊的环形结构,经典转化就是倍长原数组,破环为链

云落直接把石子合并的那一套搬了过来,看山去就比较愚笨哈!还真就记录了头尾标记(晕晕晕)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=105;
int n,a[maxn<<1];
struct node{
	int x,y;
}p[maxn<<1];
int dp[maxn<<1][maxn<<1];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		a[n+i]=a[i];
	}
	for(int i=1;i<=2*n-1;i++){
		p[i]={a[i],a[i+1]};
	}
	p[2*n]={a[2*n],a[1]};
	// for(int  i=1;i<=n*2;i++){
	// 	cout<<"Zyx "<<i<<": "<<p[i].x<<" "<<p[i].y<<endl;
	// }
	for(int len=2;len<=n;len++){
		for(int l=1;l+len-1<=2*n;l++){
			int r=l+len+-1;
			for(int k=l;k<=r-1;k++){
				dp[l][r]=max(dp[l][r],dp[l][k]+dp[k+1][r]+p[l].x*p[k].y*p[r].y);
			}
		}
	}
    int ans=0;
    for(int i=1;i<=n;i++){
    	ans=max(ans,dp[i][i+n-1]);
	}
	cout<<ans<<endl;
    return 0;
}

T3 关路灯(P1220)

链接

注意到 \(n \le 50\) 的数据范围,并且求的是一个区间的最值问题,可以考虑区间 DP(参考了一下题解区,发现这题暴搜卡卡常数都能过)

圆规正传,显然有一个结论:R 不会在一个没有亮着的灯的区间里无聊地游荡,换言之,当 R 搞定每个区间 \([l,r]\) 中所有灯的时候,他当前的位置一定是 \(l,r\) 中的任意一个

所以简单设计一下 DP 状态,记 \(f_{l,r}\) 表示关掉区间内 \([l,r]\) 所有开着的灯这一段时间总功率消耗的最小值

进一步地,根据上面那个显然的结论,我们可以加一维度状态,即 \(f_{l,r,0/1}\)——前面两维度意思一样,最后一维度 \(0\) 表示 R 结束后在左端点 \(l\) 上,\(1\) 则表示 R 结束后在右端点 \(r\)

由于 R 从位置 \(c\) 开始,显然有 \(f_{c,c,0}=f_{c,c,1}=0\)

状态设计和初始化都有了,只差一个转移方程了

比较好想的是,\(f_{l,r,0/1}\) 只可能由 \(f_{l+1,r,0/1}\) 或者 \(f_{l,r-1,0/1}\) 转移过来。进一步地,\(f_{l+1,r,0/1}\) 只能向 \(f_{l,r,0}\) 转移;\(f_{l,r-1,0/1}\) 只能向 \(f_{l,r,1}\) 转移

为什么嘞?

因为状态设计,我们这里的 \(0/1\) 表示的是 R 最后所在的位置,所以不会说 R 已经关过这里的灯最后又绕一圈回来

做到这一步其实就差不多了,方程大可以手动推理

贴个代码,辅助理解——

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=55;
int n,c,a[maxn],b[maxn];
int s[maxn],f[maxn][maxn][2];
inline void init(){
    for(int i=1;i<=n;i++){
        s[i]=s[i-1]+b[i];
    }
    memset(f,0x3f,sizeof(f));
    f[c][c][0]=0;
    f[c][c][1]=0;
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>c;
    for(int i=1;i<=n;i++){
        cin>>a[i]>>b[i];
    }
    init();
    for(int len=2;len<=n;len++){
        for(int l=1;l+len-1<=n;l++){
            int r=l+len-1;

            f[l][r][0]=min(
                f[l+1][r][0]+(a[l+1]-a[l])*(s[l]+s[n]-s[r]),
                f[l+1][r][1]+(a[r]-a[l])*(s[l]+s[n]-s[r])
            );

            f[l][r][1]=min(
                f[l][r-1][1]+(a[r]-a[r-1])*(s[l-1]+s[n]-s[r-1]),
                f[l][r-1][0]+(a[r]-a[l])*(s[l-1]+s[n]-s[r-1])
            );

        }
    }
    int ans=min(f[1][n][0],f[1][n][1]);
    cout<<ans<<endl;
    return 0;
}

T4 收集雕像(P6879)

链接

做完关路灯再做这个题就会好很多,思路大致的方向跑不偏捏——

状态设计

经典套路的就是记录 \(f_{l,r,0/1}\) 表示区间 \([l,r]\) 已被处理并且当前人物在左/右端点的答案。然而这个题并不能完全照搬上一题的套路,注意到还存在一个维度的约束——时间

但是如果直接把时间放在 DP 状态中,显然是没有任何前途的。如此巨大的数据范围还没有一个较简单的离散化方法,所以时间维度放在 DP 状态里并不可取

继续观察数据范围,发现其实我们要求的答案是一个和 \(n\) 同阶的变量,范围在 \([1,200]\) 之间。是不是可以考虑把我们的答案放入 DP 状态中捏?

OF COURSE!

进一步地,这个状态记录的信息自然是那个没有办法离散化的时间维度咯!

所以,总结一下状态定义——记 \(f_{l,r,k,0/1}\) 表示从第 \(l\) 个物品到第 \(r\) 个物品中选取了 \(k\) 个物品,此时人物在 左/右 端点的所花费的最小时间

初始化

初始化也并非易如反掌……

首先,题意给出的描述这个东西是个环,所以考虑倍长数组破环为链。然而,对于原序列的处理不能止步于此。注意到 JOI 君的初始位置不一定恰好在某一个物品上,所以考虑给 JOI 君的初始位置加入一个物品(具体可以看看代码实现)

其次,对于这个新加入的物品,也要相应的给它赋予位置和自爆时间

最后是 DP 状态的初始化,在这种 DP 状态的设计下,我们希望当 \(l,r,k\) 相同时,取到所耗时间最少的方案,所以所有状态初始化为正无穷。而对于初始位置所对应的新加入的物品,初始化为 \(0\)

状态转移

云落太菜了,没有仔细去想填表法怎么做捏……

考虑 \(f_{l,r,k,0/1}\) 会转移给哪个状态,并对其造成贡献。显然的是,肯定要对区间 \([l,r+1]\) 或者区间 \([l-1,r]\) 造成贡献。左右端点的分讨可以类比上一道题。而对于 \(k\) 自然是只需要比较自爆时间以及方案所耗时间来判定是否自增 \(1\) 咯!

总体来说,转移很好想,但是需要注意一些边界条件(不然就会像云落一样 RE)

答案计算

对于所有合法的时间,找出最大的下标 \(k\) 即可

细节处理

  1. 加入新物品后破环为链,下标范围是 \([0,2n+1]\),数组不要开太小

  2. 对于破环为链的后半段,他们的位置应当是 \(X_i + L\),这个也好理解——转一圈嘛

  3. 新加入的物品的自爆时间赋值为 \(-1\),表示第一次经过后不会对答案造成任何贡献

  4. 转移注意边界条件的判断(尤其是刷表法)

  5. 需要计算的区间长度上界是 \(n+1\),因为新加入的物品是一定会取到的

代码时间

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=205,inf=9e18;
int n,L,X[maxn<<1],T[maxn<<1];
int f[maxn<<1][maxn<<1][maxn][2];
inline void getmin(int &x,int &y){
    x=min(x,y);
    return;
}
inline void getmax(int &x,int &y){
    x=max(x,y);
    return;
}
inline void init(){
    X[0]=0;
    X[n+1]=L;
    T[0]=-1;
    T[n+1]=-1;
    for(int len=1;len<=n+1;len++){
        for(int l=0;l+len-1<=2*n+1;l++){
            int r=l+len-1;
            for(int k=0;k<=len;k++){
                f[l][r][k][0]=inf;
                f[l][r][k][1]=inf;
            }
        }
    }
    f[0][0][0][0]=0;
    f[0][0][0][1]=0;
    f[n+1][n+1][0][0]=0;
    f[n+1][n+1][0][1]=0;
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>L;
    for(int i=1;i<=n;i++){
        cin>>X[i];
        X[n+i+1]=X[i]+L;
    }
    for(int i=1;i<=n;i++){
        cin>>T[i];
        T[n+i+1]=T[i];
    }
    init();
    for(int len=1;len<=n+1;len++){
        for(int l=0;l+len-1<=2*n+1;l++){
            int r=l+len-1;
            for(int k=0;k<=len;k++){
                if(f[l][r][k][0]!=inf){
                    if(l-1>=0){
                        int tim=f[l][r][k][0]+X[l]-X[l-1];
                        getmin(f[l-1][r][k+(tim<=T[l-1])][0],tim);
                    }
                    if(r+1<=2*n+1){
                        int tim=f[l][r][k][0]+X[r+1]-X[l];
                        getmin(f[l][r+1][k+(tim<=T[r+1])][1],tim);
                    }
                }
                if(f[l][r][k][1]!=inf){
                    if(l-1>=0){
                        int tim=f[l][r][k][1]+X[r]-X[l-1];
                        getmin(f[l-1][r][k+(tim<=T[l-1])][0],tim);
                    }
                    if(r+1<=2*n+1){
                        int tim=f[l][r][k][1]+X[r+1]-X[r];
                        getmin(f[l][r+1][k+(tim<=T[r+1])][1],tim);
                    }
                }
            }
        }
    }
    int ans=0,len=n+1;
    for(int l=0;l+len-1<=2*n+1;l++){
        int r=l+len-1;
        for(int k=len;k>=0;k--){
            if(f[l][r][k][0]!=inf||f[l][r][k][1]!=inf){
                ans=max(ans,k);
                break;
            }
        }
    }
    cout<<ans<<endl;
    return 0;
}

T5 矩阵取数游戏(P1005)

链接

NOIP 的提高组真题捏,还是比较明显的区间 DP 题目

一个性质:行与行间相互独立,互不影响。然后就是行内求最大得分,注意到数据范围很小,考虑区间 DP 撒!

具体地,记 \(f_{l,r}\) 表示解决区间 \([l,r]\) 内的答案。初始化肯定都是 \(0\),重点是转移方程

当我们要消去区间 \([l,r]\) 的时候,一定是 \([1.l-1]\)\([r+1,m]\) 已经被全部消去,也就是说我们可以知道当剩余区间 \([l,r]\) 时,一定是第 \(m-len\) 步(\(len\) 表示区间 \([l,r]\) 的长度)

回到转移的方法,对于区间 \([l,r]\),显然是由区间 \([l,r-1]\) 和区间 \([l+1,r]\) 转移过来滴!那么转移的这一步贡献计算,显然是 \(a[l/r] \times 2^{m-len+1}\)。这里 \(a_i\) 表示的是当前行的第 \(i\) 个数捏!

然后就无了,需要手搓高精或者 __int128(云落不想敲高精度,只能搓一个手写输入输出的 __int128 力)

点击查看代码
#include<bits/stdc++.h>
#define int __int128
using namespace std;
const int maxn=85;
int n,m,a[maxn][maxn];
int p[maxn],f[maxn][maxn];
inline int read(){
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-'){
            f=-1;
        }
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x*f;
}
inline void write(int x){
    if(x<0){
        putchar('-');
        x=-x;
    }
    if(x>9){
        write(x/10);
    }
    putchar(x%10+'0');
    return;
}
inline void init(){
    p[0]=1;
    for(int i=1;i<maxn;i++){
        p[i]=(p[i-1]<<1);
    }
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    n=read();
    m=read();
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            a[i][j]=read();
        }
    }
    init();
    int ans=0;
    for(int k=1;k<=n;k++){
        for(int i=1;i<=m;i++){
            for(int j=1;j<=m;j++){
                f[i][j]=0;
            }
        }
        for(int len=1;len<=m;len++){
            for(int l=1;l+len-1<=m;l++){
                int r=l+len-1;
                f[l][r]=max(f[l][r],f[l+1][r]+a[k][l]*p[m-len+1]);
                f[l][r]=max(f[l][r],f[l][r-1]+a[k][r]*p[m-len+1]);
            }
        }
        // write(f[1][m]);
        // puts("");
        ans+=f[1][m];
    }
    write(ans);
    puts("");
    return 0;
}

T6 聚会(P1352)

链接

树形 DP 的第一道题目,也是一个板中之板。树形 DP 的套路就是由儿子 \(v\) 转移向父亲 \(u\)

对于这道题目,我们记录 \(f_u\) 表示 \(u\) 子树的答案,但是根本转移不了,因为父子之间的约束关系没有体现。所以,就加一维度,记 \(f_{u,0/1}\) 表示结点 \(u\) 不取/取 的答案

然后直接转移就好了嘛

\[f_{u,0} = r_u + \sum_{v \in son_u} \max(f_{v,0},f_{v,1}) \]

以及

\[f_{u,1} = \sum_{v \in son_u} f_{v,0} \]

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=6e3+5;
int n,r[maxn];
vector<int> G[maxn];
int deg[maxn],rt;
int f[maxn][2];
inline void dfs(int u,int fa){
	f[u][0]=0;
	f[u][1]=r[u];
	for(int v:G[u]){
		if(v==fa){
			continue;
		}
		dfs(v,u);
		f[u][0]+=max(f[v][0],f[v][1]);
		f[u][1]+=f[v][0];
	}
	return;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>r[i];
	}
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
		deg[u]++;
	}
	for(int i=1;i<=n;i++){
		if(deg[i]==0){
			rt=i;
			break;
		}
	}
	dfs(rt,0);
	int ans=max(f[rt][0],f[rt][1]);
	cout<<ans<<endl;
	return 0;
}

T7 树上最大和(P1122)

链接

难得出两个板题……

提示一个细节就好了,不允许有空树,如果每一朵花的“美丽程度”都是负数的情况要特判一下,答案就是那个最大的负数

(P.S. 题意没有说明根节点是 \(1\),但是是有这个条件的哈!)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=16005,inf=9e18;
int n,a[maxn];
vector<int> G[maxn];
int f[maxn];
inline void dfs(int u,int fa){
    f[u]=a[u];
    for(int v:G[u]){
        if(v==fa){
            continue;
        }
        dfs(v,u);
        f[u]+=max(f[v],0ll);
    }
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    int mx=-inf;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        mx=max(mx,a[i]);
    }
    for(int i=1;i<=n-1;i++){
        int u,v;
        cin>>u>>v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    if(mx<0){
        cout<<mx<<endl;
        return 0;
    }
    dfs(1,0);
    int ans=-inf;
    for(int i=1;i<=n;i++){
        ans=max(ans,f[i]);
    }
    cout<<ans<<endl;
    return 0;
}

T8 苹果树(P2015)

链接

树形 DP 现在可是出的越来越花哨了捏。云落好菜,不知道这个东西能不能叫做树上的背包问题

我们记 \(f_{u,i}\) 表示 \(u\) 子树内选出 \(i\) 条边的答案(这个状态设计应该很显然,多做点背包题就有感觉了)

考虑转移

树上的动态规划问题还是套路式地 \(v \to u\) 转移,所以注意力惊人时间到,可以得出如下转移方程:

\[f_{u,k1} = \max_{1 \le k1 \le \min (q,sz_u),0 \le k2 \le \min (k1-1,sz_v)} \Big \lbrace f_{u,k1-k2-1}+f_{v,k2}+w(u,v) \Big \rbrace \]

额,好叭,一点一点解释——\(k1\) 表示当前状态 \(u\) 子树内要选中 \(k1\) 条边,\(k2\) 表示对于枚举出的 \(u\) 的一个儿子 \(v\)\(v\) 子树内要填入 \(k2\) 条边,\(w(u,v)\) 表示无向边 \((u,v)\) 的边权(即该树枝上苹果的数量)

两个范围——

\(1 \le k1 \le \min(q,sz_u)\):下界好理解,上界首先不能超过给定的边数限制 \(q\),其次不能完全填满整棵子树,所以也不能超过子树大小

\(0 \le k2 \le \min(k1-1,sz_v)\):下界也是好理解的,上界 \(sz_v\) 同理。而对于 \(k1-1\),注意到一个隐藏条件,如果保留结点 \(u\) 对应的子树,那么 \(u\) 的返祖链是都需要被保留的。也就是说,枚举出的 \(k2\) 最多为 \(k1-1\),因为还需要保留无向边 \((u,v)\)

内部的转移方程——

\(f_{v,k2}\) 是显然的,\(f_{u,k1-k2-1}\) 也是显然的(\(-1\) 同样是因为需要保留无向边 \((u,v)\) 所造成的贡献),\(w(u,v)\) 也很好理解……

实现细节

众所周知,这是一个 \(01\) 背包,略微提示一下——循环的正序/倒序

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=105;
int n,q;
int head[maxn],tot;
struct Edge{
    int to,nxt,val;
}e[maxn<<1];
int sz[maxn],f[maxn][maxn];
inline void add(int u,int v,int w){
    e[++tot].to=v;
    e[tot].val=w;
    e[tot].nxt=head[u];
    head[u]=tot;
    return;
}
inline void dfs(int u,int fa){
    sz[u]=1;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to,w=e[i].val;
        if(v==fa){
            continue;
        }
        dfs(v,u);
        sz[u]+=sz[v];
        for(int k1=min(sz[u],q);k1>=1;k1--){
            for(int k2=min(sz[v],k1-1);k2>=0;k2--){
                f[u][k1]=max(f[u][k1],f[u][k1-k2-1]+f[v][k2]+w);
            }
        }
    }
    return;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>q;
    for(int i=1;i<=n-1;i++){
        int u,v,w;
        cin>>u>>v>>w;
        add(u,v,w);
        add(v,u,w);
    }
    dfs(1,0);
    cout<<f[1][q]<<endl;
    return 0;
}

T9 Sequence(P7914)

链接

2021 年的,还挺热乎

\(f_{l,r}\) 表示区间 \([l,r]\) 的方案数,直接区间 DP 转移——会过不了样例。究其原因,是我们直接 DP 会算重一部分,比如:

()*()*()

于是乎,我们考虑增加一维,细化一下“超级括号序列”的种类,避免重复。我们记——

\(f_{l,r,0}\) 表示区间内全“*”串,形如 “********”

\(f_{l,r,1}\) 表示最外层只有一组括号匹配,形如“ ( * ... * ) ”

\(f_{l,r,2}\) 表示前括号序列后连续 “*”,形如 “ (...) **** ”

\(f_{l,r,3}\) 表示括号匹配的情况(包含 \(f_{l,r,1}\) 的情况),形如 “( ... ) ... ( ... )”

\(f_{l,r,4}\) 表示前连续 “*” 后括号序列,形如 “ **** ( ... ) ”

简述一下转移过程,具体就看代码吧……

\(f_{l,r,0}\) 直接特判

\(f_{l,r,1}\) 可以从 \(f_{l,r,0/2/3/4}\) 转移

\(f_{l,r,2}\) 好做捏,可以枚举断点 \(k\),拆分出前面的连续括号序列 \([l,k]\) 以及后面的连续 “*” \([k+1,r]\),即 \(f_{l,k,3} \times f_{k+1,r,0}\)

\(f_{l,r,3}\) 就比较另类,依旧考虑枚举断点 \(k\)\([l,k]\) 是一段括号序列开头,任意东西结尾的子串(\(f_{l,k,2}+f_{l,k,3}\)),\([k+1,r]\) 这一部分直接 \(f_{k+1,r,1}\) 转移即可

\(f_{l,r,4}\) 类比 \(f_{l,r,2}\),也很好做捏,直接 \(f_{l,k,0} \times f_{k+1,r,3}\)

答案的计算显然是 \(f_{1,n,3}\),注意取模!

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=505,mod=1e9+7;
int n,k;
char s[maxn];
int dp[maxn][maxn][5];
signed main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>s[i];
	}
	for(int i=1;i<=n;i++){
		dp[i][i-1][0]=1;
	}
	for(int len=1;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1; 
			if(len<=k&&dp[l][r-1][0]&&(s[r]=='*'||s[r]=='?')){
				dp[l][r][0]=1;
			}
			if(len>=2){
				if((s[l]=='('||s[l]=='?')&&(s[r]==')'||s[r]=='?')){
					dp[l][r][1]=(dp[l+1][r-1][0]+dp[l+1][r-1][2]+dp[l+1][r-1][3]+dp[l+1][r-1][4])%mod;
				}
				for(int k=l;k<=r-1;k++){
					dp[l][r][2]=(dp[l][r][2]+dp[l][k][3]*dp[k+1][r][0])%mod;
					dp[l][r][3]=(dp[l][r][3]+(dp[l][k][2]+dp[l][k][3])*dp[k+1][r][1])%mod;
					dp[l][r][4]=(dp[l][r][4]+dp[l][k][0]*dp[k+1][r][3])%mod;
				}
			}
            dp[l][r][3]=(dp[l][r][3]+dp[l][r][1])%mod;
		}
	}
	cout<<dp[1][n][3]<<endl;
	return 0;
}

T10 Coloring(P4170)

链接

看到区间涂色,以及求最小涂色次数,一眼区间 DP。自然地,记 \(f_{l,r}\) 表示区间 \([l,r]\) 涂色需求被满足的最小涂色次数

初始化是显然的,对于 \(\forall i \in [1,n],f_{i,i}=1\)。是时候,考虑转移咯!

枚举断点 \(k\),拼接区间,方程形如 \(f_{l,r}=f_{l,k}+f_{k+1,r}\)。然而大概率过不了样例,注意到自己给出的答案偏大,为啥嘞?

因为在上面区间拼接的过程中,我们是默认两个区间是彼此独立的,但是如果 \(\text{col}_l = \text{col}_r\),显然两个区间进行合并是少花费一次涂色次数的,所以加入一个判断即可(代码实现超级简单的好叭)

感觉没有蓝题难度

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=55;
char s[maxn];
int n,f[maxn][maxn];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>(s+1);
    int n=strlen(s+1);
    memset(f,0x3f,sizeof(f));
    for(int i=1;i<=n;i++){
        f[i][i]=1;
    }
    for(int len=2;len<=n;len++){
        for(int l=1;l+len-1<=n;l++){
            int r=l+len-1;
            for(int k=l;k<=r-1;k++){
                f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
            }
            if(s[l]==s[r]){
                f[l][r]--;
            }
        }
    }
    cout<<f[1][n]<<endl;
    return 0;
}

T11 树上染色(P3177)

链接

做这道题之前建议完成 T8,两者思路是极类似的

众所周知,树形 DP 的状态设计并不是很困难,尤其是这种类似树上背包的问题,状态设计都是具有一定套路性的。记 \(f_{u,i}\) 表述 \(u\) 子树内填入 \(i\) 个黑色结点的答案

初始化也是很显然,都赋值为 \(0\) 即可,日常——考虑转移

注意到统计每个点对的贡献是有后效性的,故此,不妨统计边的贡献。对于一条边 \((v,u)\)(令 \(u\) 满足 \(u=fa_v\)),它可以将整棵树拆分成 \(v\) 子树以及 \(v\) 子树以外的部分,共两个点集。而对于 \((v,u)\) 的贡献就是这两个点集中同色点的乘积,最后再乘上边权 \(w(u,v)\) 即可

代码实现大概长这样——

int val=k2*(k-k2)*w+(sz[v]-k2)*(n-k-sz[v]+k2)*w;

简单解释一下,\(k2\) 表示 \(v\) 子树内黑点个数,\(n,k\) 如题意,\(w\)\((v,u)\) 的边权,\(sz_v\)\(v\) 子树的大小。第一项统计的是黑色点在 \((u,v)\) 上造成的贡献,第二项统计的是白色点在 \((u,v)\) 上造成的贡献

所以,转移方程大概也可以写出来了,形如:

\[f_{u,k1}= \max_{0 \le k1 \le k,max(0,k1-sz_u+sz_v) \le k2 \le min(sz_v,k1)} \Big \lbrace f_{u,k1-k2}+f_{v,k2}+val \Big \rbrace \]

额,\(val\) 就是上面最上面那一串统计贡献的式子,答案显然是 \(f_{1,k}\)

代码实现上,需要强调的是,\(k1\) 必须要倒序更新,否则式子推着推着就左脚踩右脚原地升天力!\(k2\) 倒是没有那么多奇奇怪怪的要求……

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e3+5;
int n,k;
int head[maxn],tot;
struct Edge{
	int to,nxt,val;
}e[maxn<<1];
int sz[maxn],f[maxn][maxn];
inline void add(int u,int v,int w){
	e[++tot].to=v;
	e[tot].val=w;
	e[tot].nxt=head[u];
	head[u]=tot;
	return;
}
inline void dfs(int u,int fa){
	sz[u]=1;
	for(int i=head[u];i;i=e[i].nxt){
		int v=e[i].to,w=e[i].val;
		if(v==fa){
			continue;
		}
		dfs(v,u);
		sz[u]+=sz[v];
		for(int k1=k;k1>=0;k1--){
			for(int k2=max(k1-sz[u]+sz[v],0ll);k2<=min(sz[v],k1);k2++){
				int val=k2*(k-k2)*w+(sz[v]-k2)*(n-k-sz[v]+k2)*w;
				f[u][k1]=max(f[u][k1],f[u][k1-k2]+f[v][k2]+val);
			}
		}
	}
	return;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n-1;i++){
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
	}
	dfs(1,0);
	cout<<f[1][k]<<endl;
	return 0;
}

后记

也是终于完工了(明明比线段树合并简单,但为什么耗时更长了……)

完结撒花!

posted @ 2025-03-02 09:13  sunxuhetai  阅读(23)  评论(0)    收藏  举报