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\) 即可
细节处理
-
加入新物品后破环为链,下标范围是 \([0,2n+1]\),数组不要开太小
-
对于破环为链的后半段,他们的位置应当是 \(X_i + L\),这个也好理解——转一圈嘛
-
新加入的物品的自爆时间赋值为 \(-1\),表示第一次经过后不会对答案造成任何贡献
-
转移注意边界条件的判断(尤其是刷表法)
-
需要计算的区间长度上界是 \(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\) 不取/取 的答案
然后直接转移就好了嘛
以及
点击查看代码
#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\) 转移,所以注意力惊人时间到,可以得出如下转移方程:
额,好叭,一点一点解释——\(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)\) 上造成的贡献
所以,转移方程大概也可以写出来了,形如:
额,\(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;
}
后记
也是终于完工了(明明比线段树合并简单,但为什么耗时更长了……)
完结撒花!

浙公网安备 33010602011771号