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)

链接

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

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

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

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

由于 R 从位置 c 开始,显然有 fc,c,0=fc,c,1=0

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

比较好想的是,fl,r,0/1 只可能由 fl+1,r,0/1 或者 fl,r1,0/1 转移过来。进一步地,fl+1,r,0/1 只能向 fl,r,0 转移;fl,r1,0/1 只能向 fl,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)

链接

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

状态设计

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

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

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

OF COURSE!

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

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

初始化

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

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

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

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

状态转移

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

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

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

答案计算

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

细节处理

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

  2. 对于破环为链的后半段,他们的位置应当是 Xi+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 撒!

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

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

回到转移的方法,对于区间 [l,r],显然是由区间 [l,r1] 和区间 [l+1,r] 转移过来滴!那么转移的这一步贡献计算,显然是 a[l/r]×2mlen+1。这里 ai 表示的是当前行的第 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

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

然后直接转移就好了嘛

fu,0=ru+vsonumax(fv,0,fv,1)

以及

fu,1=vsonufv,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 现在可是出的越来越花哨了捏。云落好菜,不知道这个东西能不能叫做树上的背包问题

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

考虑转移

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

fu,k1=max1k1min(q,szu),0k2min(k11,szv){fu,k1k21+fv,k2+w(u,v)}

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

两个范围——

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

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

内部的转移方程——

fv,k2 是显然的,fu,k1k21 也是显然的(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 年的,还挺热乎

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

()*()*()

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

fl,r,0 表示区间内全“*”串,形如 “********”

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

fl,r,2 表示前括号序列后连续 “*”,形如 “ (...) **** ”

fl,r,3 表示括号匹配的情况(包含 fl,r,1 的情况),形如 “( ... ) ... ( ... )”

fl,r,4 表示前连续 “*” 后括号序列,形如 “ **** ( ... ) ”

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

fl,r,0 直接特判

fl,r,1 可以从 fl,r,0/2/3/4 转移

fl,r,2 好做捏,可以枚举断点 k,拆分出前面的连续括号序列 [l,k] 以及后面的连续 “*” [k+1,r],即 fl,k,3×fk+1,r,0

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

fl,r,4 类比 fl,r,2,也很好做捏,直接 fl,k,0×fk+1,r,3

答案的计算显然是 f1,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。自然地,记 fl,r 表示区间 [l,r] 涂色需求被满足的最小涂色次数

初始化是显然的,对于 i[1,n],fi,i=1。是时候,考虑转移咯!

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

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

感觉没有蓝题难度

点击查看代码
#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 的状态设计并不是很困难,尤其是这种类似树上背包的问题,状态设计都是具有一定套路性的。记 fu,i 表述 u 子树内填入 i 个黑色结点的答案

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

注意到统计每个点对的贡献是有后效性的,故此,不妨统计边的贡献。对于一条边 (v,u)(令 u 满足 u=fav),它可以将整棵树拆分成 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) 的边权,szvv 子树的大小。第一项统计的是黑色点在 (u,v) 上造成的贡献,第二项统计的是白色点在 (u,v) 上造成的贡献

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

fu,k1=max0k1k,max(0,k1szu+szv)k2min(szv,k1){fu,k1k2+fv,k2+val}

额,val 就是上面最上面那一串统计贡献的式子,答案显然是 f1,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 @   sunxuhetai  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
点击右上角即可分享
微信分享提示