DP做题合集

第一题 P7074 [CSP-J2020] 方格取数

做法【dp】

阶段

因为只能往左不能往右,所以我们可以以一列作为一个阶段。

又因为路线不能重复,所以在一列之中,只能一直向上或一直向下,所以我们分类讨论。

PS:妙啊,通过分类讨论来解决路径的后效性问题。

状态定义

\(dp_{i,j,t}\) 表示位于第 \(i\) 行 ,第 \(j\) 列,当 \(t\) \(=\) \(1\) 时,表示从上方来到当前格,当 \(t\) \(=\) \(0\) 时,表示从下方来到当前格,时得到的最大值。

状态转移方程

\(t = 1\) 时:

\(dp_{i,j,t} = \max (dp_{i,j-1,0},dp_{i,j-1,1},dp_{i-1,j,1}) + a_{i,j}\)

\(t = 0\) 时:

\(dp_{i,j,t} = \max (dp_{i,j-1,0},dp_{i,j-1,1},dp_{i+1,j,0}) + a_{i,j}\)

初始化

因为起点在左上角,所以第一列只能自上往下走。

$dp_{i,1,1} = dp_{i-1,1,1} + a_{i,1} $

code

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define ll long long
using namespace std;
ll n,m;
ll a[1005][1005],v[1005][1005][2],f[1005][1005];
int main(){
	scanf("%lld%lld",&n,&m);
	for(ll i=1;i<=n;i++)
		for(ll j=1;j<=m;j++) scanf("%lld",&a[i][j]);
	memset(v,0xcf,sizeof(v));
	for(int i=1;i<=n;i++) f[i][1]=f[i-1][1]+a[i][1];
	for(int j=2;j<=m;j++){
		for(int i=1;i<=n;i++) v[i][j][1]=max(f[i][j-1],v[i-1][j][1])+a[i][j];
		for(int i=n;i>=1;i--) v[i][j][0]=max(f[i][j-1],v[i+1][j][0])+a[i][j];
		for(int i=1;i<=n;i++) f[i][j]=max(v[i][j][0],v[i][j][1]); 
	}
	cout<<f[n][m]<<endl;
	return 0;
}

summary

这道题与普通的区间 \(DP\) 不同,不止可以往左,往下走,还可以往上走,这样就无法满足 \(DP\) 的无后效性。

为了解决这个问题,于是采用了分类讨论的方法。

因为只能往左,所以我们可以把每列分成一个阶段。因为路径不能重叠,所以对于每一列,只能一直向上或者一直向下

于是我们在正常的区间 \(DP\) 上再加一维来记录向上 or 向下走。

\(2022.8.17\)

第二题 P5662 [CSP-J2019] 纪念品

思路分析

题目中说了,求在最后将所有纪念品卖掉后能拥有的最多金币数量。

那么我们就可以设 \(dp_{i}\) 表示在第 \(i\) 天把所有纪念品都卖掉后能拥有的最多金币的数量。

由于对于第 \(i-1\) 天,卖出纪念品和买入纪念品的价格相同,因此,我们可以在第 \(i-1\) 天先将手上所有的纪念品都卖掉。然后利用与次日相同纪念品的价格差挑选最佳纪念品从中赚取利润(是的,这就叫中间商赚差价)。即可求出第 \(i\) 天初始得到的最大金币(这里指的也是在第 \(i\) 天将所有纪念品卖掉后的,即\(dp_i\) )。

那么他的状态转移方程就是:

\(dp_{i}=\max\){\(dp_{i-1}-在 i-1 天买纪念品的总花费+在第 i 天卖纪念品所得到的总收益\)}

还有变量无法表示出来,我们就将他加入状态转移方程中。(相信有聪明的小朋友已经看出来接下来就是标准的完全背包了)

\(dp_{i,j}\) 表示用第 \(i\) 天把所有纪念品都卖掉后能拥有的最多金币花费\(j\)元购买纪念品所能得到的最大价值。

花费的是第 \(i\) 天购买纪念品的价格,得到的价值为第 \(i+1\) 天卖出后得到的收益。

对于一个纪念品 \(k\)

\(dp[i][j]=\max(dp[i][j],dp[i][j-val[i][k]]+val[i+1][k])\)

\(i+1\) 天能得到的最多金币数为:

\(ans=max(ans,num-j+dp[i][j])\)

\(num\) 是第 \(i\) 天获得的最大金币数, \(ans\) 是第 \(i+1\) 天能得到的最多金币数。

写代码的过程中可以发现我们己经枚举了天数\(t\),且在状态转移方程中 \(i\) 丝毫不动,因此可以将其省略。

code

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
int T,n,m;
int dp[10005],val[105][1005];
int main(){
	scanf("%d%d%d",&T,&n,&m);
	for(int i=1;i<=T;i++)
		for(int j=1;j<=n;j++) scanf("%d",&val[i][j]);
	
	int ans=m;
	for(int t=1;t<T;t++){
		memset(dp,0xcf,sizeof(dp));
		dp[0]=0;
		for(int i=1;i<=n;i++){
			for(int j=val[t][i];j<=ans;j++){
				dp[j]=max(dp[j],dp[j-val[t][i]]+val[t+1][i]);
			}
		}
		int num=ans;
		for(int i=1;i<=num;i++) ans=max(ans,num-i+dp[i]);
	}
	cout<<ans<<endl;
	return 0;
}

summary

通过这题,我们可以总结出,一题DP可以先将状态设为题目要求的东西,再列出状态转移方程。如果状态转移方程有些部分无法表示出来,就将他加入状态之中。

\(2022.8.18\)

第三题 P1018 [NOIP2000 提高组] 乘积最大

前言

事先声明!博主是不会写高精的屑。因此此题只拿到了开 \(LL\)\(\color{orange}{60}\) 分。

但这并不妨碍我练 \(DP\)

思路辨析

很容易想到,以前 \(i\) 个数的部分作为一个阶段变量。

有了具体的数量,又很自然的想到将钥匙的个数作为一个变量加进去,也就是 \(j\)

诶,好像能行,再看看。

综上,\(dp_{i,j}\) 表示的是前 \(i\) 个数用 \(j\) 个乘号隔开所能得到的最大乘积。

设想一下,当我们知道如上所述的信息时,要怎么通过它得到下一阶段的信息,或它是如何由其它阶段推来的呢?

显然第二种更好推。

已知钥匙的个数,枚举最后一个钥匙所在的位置(这里记为 \(r\) ),可以将前 \(i\) 个数分成两部分。

前半部分是前 \(r\) 个数用 \(j-1\) 个乘号断开所得到的乘积最大值,后半部分则是 \(N\) 个字符中第 \(r+1\) 个字符到第 \(i\) 个字符所组成的数,用两者之积来更新答案。

因此,状态转移方程为

\(dp_{i,j} = \max( dp_{r,j-1} * nu_{r+1,i} , dp_{i,j})\).

注:\(nu_{r+1,i}\)就是 \(N\) 个字符中第 \(r+1\) 个字符到第 \(i\) 个字符所组成的数。

code

#include<iostream>
#include<cstdio>
#include<cmath>
#define ll long long
using namespace std;
ll n,k,a[45],nu[45][45],dp[45][10];
int main(){
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++) scanf("%1d",&a[i]);//“%1d”每次只读一个数字
	
	for(int i=1;i<=n;i++) nu[i][i]=a[i];
	for(int i=1;i<=n;i++){
		for(int j=i+1;j<=n;j++){
			nu[i][j]=nu[i][j-1]*10+a[j];
		}
	}
	
	//这一段是对nu的预处理
	
	for(int i=1;i<=n;i++) dp[i][0]=nu[1][i];//初始化
	
	for(ll i=1;i<=n;i++){
		for(int j=1;j<min(i,k+1);j++){//i个数最多只能放i-1个乘号,所以和k+1取min
			for(int r=1;r<i;r++){
				dp[i][j]=max(dp[r][j-1]*nu[r+1][i],dp[i][j]);
			}
		}
	}
	cout<<dp[n][k]<<endl;
	return 0;
}

summary

对状态的描述好像有进步!继续加油!

\(2022.12.27\)

第四题 P1586 四方定理

思路分析

对于一个数 \(i\) ,它可能由 \(j\) (\(1\le j \le 4\)) 个平方数组成。

我们不妨设 \(dp_{i,j}\) 为数 \(i\)\(j\) 个平方数所组成的方案。

那么很容易想到 \(dp_{i,j} += dp_{i-k*k,j-1}\) \((k*k\le i)\)。即枚举所有小于 \(i\) 的平方数,加上包含其的方案数。

于是我们就能得到以下代码

	dp[0][0]=1;
	for(int i=1;i<=32768;i++){
		for(int j=1;j<=4;j++){
			for(int k=1;k*k<=i;k++){
					dp[i][j]+=dp[i-rec[k]][j-1];
			}
		}
	}

乍看似乎没什么问题。但输入 \(5\) 后会发现,标准答案是 \(1\) ,输出却是 \(2\)

这是怎么回事呢?仔细研究后会发现。如果这么枚举的话,$5=2^2 + 1^2 $ 和 \(5 = 1^2+2^2\) 被当成两种方案被统计了两次。

此时,需要交换枚举顺序,令一种方案被其和数中的最高次方平方数累计。

简单来说,就是令 \(5\)\(2^2\) 统计,而不是 \(1^2\)

于是乎得到以下代码。

	dp[0][0]=1;
	for(int i=1;i*i<=32768;i++){
		for(int j=i*i;j<=32768;j++){
			for(int k=1;k<=4;k++){
				dp[j][k]+=dp[j-rec[i]][k-1];
			}
		}
	}

至此,此题就可以 \(\color{green}{AC}\) 啦。

code

点击查看代码
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int rec[32770],dp[32770][5];
int t,n;
int main(){
	scanf("%d",&t);
	for(int i=1;i*i<=32768;i++) rec[i]=i*i;
	dp[0][0]=1;
	for(int i=1;i*i<=32768;i++){
		for(int j=i*i;j<=32768;j++){
			for(int k=1;k<=4;k++){
				dp[j][k]+=dp[j-rec[i]][k-1];
			}
		}
	}
	for(int i=1;i<=t;i++){
		scanf("%d",&n);
		int num=0;
		for(int j=1;j<=4;j++) num+=dp[n][j];
		cout<<num<<endl;
	}
	return 0;
} 

summary

对于去重似乎打开了新世界的大门。

仔细回想,通过规定顺序来去重的方法已经不是第一次见了。

在深搜中我们经常通过增加变量 \(last\) 来固定枚举顺序以达到剪枝的作用。这里剪掉的,恰好就是重复的方案。

在质数筛中,线性筛法就是通过保证每个合数 \(i * p\) 只会被它最小质因子 \(p\) 筛一次,从而优化掉了被重复筛的合数。和此题是不是有着异曲同工之妙!

\(2023.1.2\)

第五题 P2426 删数

题目分析

由于对于题目所得的最优删法,与删除的顺序无关,因此我们可以默认从前往后删片段。

\(dp_i\) 表示删除前 \(i\) 个数所得到的最大价值。

对于第 \(i\) 个数,它可以选择独自删除 \(i\) 。状态转移方程为

$ dp_i $ \(=\) $ dp_{i-1}$ \(+\) \(a_i\)

亦可以选择与前面的数一起删掉。求此时与哪几个数一起删得最大值。

\(dp_i\) \(=\) $dp_{j-1} + $$abs (a_i-a_j)*(i-j+1)$ \((1\le j < i)\)

表示删除 \(j-i\) 段的数。

code

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,a[105],dp[105];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	dp[1]=a[1]; 
	for(int i=2;i<=n;i++){
		for(int j=1;j<i;j++){
			dp[i]=max(dp[i],dp[j-1]+abs(a[i]-a[j])*(i-j+1));
		}
		dp[i]=max(dp[i],dp[i-1]+a[i]);
	}
	cout<<dp[n]<<endl;
	return 0;
} 

summary

今天这题看似很简单,想明白却不容易。

本来一开始定义的是 \(dp_{i,j}\) 表示删除 \(i-j\) 段所获得的最大价值。

但接下来就遇到难点了,我们要如何枚举断点 \(k\)

如果只是将断点 \(k\) 看做将 \(i-j\) 字符分成两端所得到的最大价值,那可出大问题了。

对于一段字符,它的最优断法不一定是断成两段。

但是,若断点\(k\)表示其中有一段删除的是\(k-j\),而删除\(i-(k-1)\) 段所获得的最大价值为 \(dp_{i,k-1}\) 。这...状态转移方程不就又出来了。

至此,根本思路与题目分析已无差别。

因此,在思考问题时,一定要注意从问题本身出发,来找状态转移方程。

\(2023.1.2\)

第六道 P1005 [NOIP2007 提高组] 矩阵取数游戏

前言

今天依旧是不写高精的一天呢!(是的,这位作者又只拿了开 \(LL\)\(\color{yellow}{60}\) 分)

思路描述

看到数据 \(n,m \le 80(30)\) 就知道数组可以任性开,心理有个底后,再来看题目。

状态描述

首先肯定要来一个 \(dp_{i,j}\) 来表示第 \(i\) 次时取第 \(j\) 行的数。

对于每一次放置,我们要考虑到的是之前每一次都取到什么,也就是现在的头和尾分别是哪两个数

想明白这一点,就可以描述状态了。

\(dp_{i,j,k,t}\) 表示第 \(i\) 次时取第 \(j\) 行的数,对于第 \(j\) 行,它的行首被取了 \(k\) 个数,他的行尾被取了 \(t\) 个数。

由于 $t = i - k $ ,当 \(i,k\) 确定时,\(t\) 也一定唯一,因此可以省略。

状态转移方程

描述出状态了,状态转移方程还会远吗?

显然有

\(dp_{i,j,k} = \max(dp_{i-1,j,k-1}+val(i,j,k),dp_{i-1,j,k}+val(i,j,m-(i-k)+1))\)

\(val(x,y,z)\) 表示第 \(x\) 次时取位于第 \(y\) 行第 \(z\) 列的数所能获得的得分。

\(\max\) 中的两者分别对应了第 \(i\) 次时,在第 \(j\) 行取队首 \(or\) 队尾的情况。

code

#include<iostream>
#include<cstdio>
#include<cmath>
#define ll long long
using namespace std;
int n,m;
ll a[85][85],dp[85][85][85];
int bas[31];
int main(){
	scanf("%d%d",&n,&m);
	bas[0]=1;
	for(int i=1;i<=30;i++) bas[i]=bas[i-1]*2;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) scanf("%d",&a[i][j]);
		
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			dp[i][j][i]=dp[i-1][j][i-1]+a[j][i]*bas[i],dp[i][j][0]=dp[i-1][j][0]+a[j][m-i+1]*bas[i];//这两种情况比较特殊,所以单独列。
			for(int k=1;k<i;k++){
				dp[i][j][k]=max(dp[i-1][j][k-1]+a[j][k]*bas[i],dp[i-1][j][k]+a[j][m-(i-k)+1]*bas[i]);
			}
		}
	}
	ll ans=0;
	for(int i=1;i<=n;i++){
		ll max_num=0;
		for(int j=0;j<=m;j++)
			max_num=max(max_num,dp[m][i][j]);
		ans+=max_num;
	}
	cout<<ans<<endl;
	return 0; 
}

ps:经过作者后续习惯性翻翻题解(发现原来区间DP也可以做),以及打输出时的共同启发,发现实际上我们只需要分别枚举对于每一行是的最优解,加起来就可以了。因此状态中表示行的那一维可以省略。然后就有了以下代码。

#include<iostream>
#include<cstdio>
#include<cmath>
#define ll long long
using namespace std;
int n,m;
ll a[85][85],dp[85][85];
int bas[31];
int main(){
	scanf("%d%d",&n,&m);
	bas[0]=1;
	for(int i=1;i<=30;i++) bas[i]=bas[i-1]*2;

	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) scanf("%d",&a[i][j]);

	ll ans=0,max_num;	
	for(int j=1;j<=n;j++){
		
		for(int i=1;i<=m;i++){
			dp[i][i]=dp[i-1][i-1]+a[j][i]*bas[i],dp[i][0]=dp[i-1][0]+a[j][m-i+1]*bas[i];
			for(int k=1;k<i;k++){
				dp[i][k]=max(dp[i-1][k-1]+a[j][k]*bas[i],dp[i-1][k]+a[j][m-(i-k)+1]*bas[i]);
			}
		}
		max_num=0;
		for(int i=0;i<=m;i++) max_num=max(max_num,dp[m][i]);
		ans+=max_num;
	}
	
	cout<<ans<<endl;
	return 0; 
}

事实上没太大区别,毕竟它的数据范围可以让我任性开(首尾呼应.jpg(确信))。

summary

对于省略维数有了更深刻的理解。

  • 可以用其他维度表示的可以省略。

  • 可以通过分开解决时不需要整体来定义。

\(2023.1.12\)

第七、八道 P1352 没有上司的舞会+P1122 最大子树和(树形DP入门)

前言

今日偶然打开 \(oi-wiki\),发现树形 \(DP\) 例题正好是之前在洛谷上鸽着的一道题。所以......

\(\color{red}{很高兴以这样的方式认识你,树形 DP !}\)

这例题造的太好了,简直是无痛入门(感动.jpg)

P1352 没有上司的舞会

题目传送门~

思路剖析

状态定义

\(dp_i\) 表示的是以 \(i\) 为根节点的子树所获得的最大价值。

由于每个节点代表着一位人物,有来与不来两种状态,所以再加一维状态变量。

\(dp_{i,0}\) 表示以 \(i\) 为根节点的子树所能获得的最大价值,且这位人物没来。 \(dp_{i,1}\) 则对应来了的状态。

状态转移方程

 现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r_i。
 但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

根据题意描述,容易得出状态转移方程:

\(dp_{i,0} += max (dp_{j,0},dp_{j,1})\)

\(dp_{i,1} += dp_{j,0}\)

\(j\) 指的是 \(i\) 的子节点,且显然 \(dp_{i,1}\) 的初始值为 \(r_i\)

code

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,a[6005];
int head[6005],nex[6005],edge[6005],tot;
int vis[6005],dp[6005][2];
void dfs(int x){
	dp[x][1]=a[x];
	for(int i=head[x];i;i=nex[i]){
		int y=edge[i];
		dfs(y);
		dp[x][1]+=dp[y][0];
		dp[x][0]+=max(dp[y][0],dp[y][1]);
	}
	return;
} 
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<n;i++){
		int l,k;
		scanf("%d%d",&l,&k);
		nex[++tot]=head[k];
		head[k]=tot;
		edge[tot]=l;
		vis[l]=1;
	}
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			dfs(i);
			cout<<max(dp[i][0],dp[i][1])<<endl;
			return 0;
		}
	}
}

P1122 最大子树和

题目传送门~

思路剖析

谁是根节点

由于这题是无向图(但由于以 \(n-1\) 条边相连接,所以本质与树并无太大区别),所以要讨论以谁作为根节点。

根节点之所以重要,是因为在递归过程中,我们已经默认根节点所代表的那束花已经被保留了,但根节点代表的花不一定在最优解的集合之中。

仔细模拟后,不难发现,对于以 \(i\) 为根节点的子树,\(dp_i\) 往下为最优解,而往上由于还未更新,因此相当于剪去 \(dp_i\) 与其根节点的枝桠。

进一步推理,无论通过哪个节点作为根节点,再递归的过程中,其实已经变相枚举了将其剪去的种种情况,所以,只需要在过程中取最优解即可。

状态定义+状态转移方程

这点比较好理解,所以合并在一起阐述。

\(dp_i\) 表示以 \(i\) 为根节点的子树所获得的最大美丽值。

显然有

\(dp_i+=max(dp_j,0)\)

\(j\) 为子节点,当其所带来的价值为负数时,不如直接剪掉。

code

有几处雷点在注释中标记出来了(都是血泪教训啊QAQ)

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,ans=-0x3f3f3f3f;//答案可能为负!要初始化为负无穷
int head[16005],nex[35005],edge[35005],tot;//由于是双向边,所以空间要开双倍
int dp[16005],vis[16005];
void dfs(int x){
	vis[x]=1;//不要在循环内标记,否则标记不到根节点本身。
	for(int i=head[x];i;i=nex[i]){
		int y=edge[i];
		if(vis[y]) continue;
		dfs(y);
		if(dp[y]<=0) continue;
		dp[x]+=dp[y]; 
	}
	ans=max(ans,dp[x]);
	return;
} 
void add(int l,int k) {nex[++tot]=head[k],head[k]=tot,edge[tot]=l;}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&dp[i]);
	for(int i=1;i<n;i++){
		int l,k;
		scanf("%d%d",&l,&k);
		add(l,k);
		add(k,l);
	}
	dfs(1);
	cout<<ans<<endl;
	return 0;
}

\(2023.1.18\)

第九题 P1387 最大正方形

题目分析

\(dp_{i,j}\) 表示以 \(i,j\) 为右下角的最大正方形的边长。

状态转移方程为:

\(dp_{i,j} = \min(dp_{i,j-1},dp_{i-1,j},dp_{i-1,j-1})+1\)

为什么取最大边长却要用 \(\min\) 来转移呢?

因为在三者中取最小,等于另外两者一定大于最小的值,不需要考虑另外两者比边长小的问题。且以最小的值 \(+1\) 为边长进行状态转移,一定会形成为以 \(i,j\) 为右下角的最大正方形,再大一点都不符合条件,不会形成正方形。

code


#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,m,dp[105][105],a[105][105],ans;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++) scanf("%d",&a[i][j]);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(a[i][j]) dp[i][j]=min(dp[i-1][j],min(dp[i-1][j-1],dp[i][j-1]))+1;
			ans=max(ans,dp[i][j]);
		}
	}
	cout<<ans<<endl;
	return 0;
}

\(2023.1.29\)

第十题 P3147 [USACO16OPEN]262144 P

题目分析

此题为区间 \(DP\) ,却与一般的 \(DP\) 题不同。能够看出是两个相邻区间合并,但是却不知道具体是哪两个区间。

因此,我们需要将区间加入状态描述中。左端点,区间长度,右端点三选二。这里选择左端点和区间长度。

\(dp_{i,j}\) 表示以 \(j\) 为左端点合成数字 \(i\) 的区间长度。当区间长度为 \(0\) 时,表示不存在这样的区间。

则可得到状态转移方程:

\(dp[i][j]=dp[i-1][j]+dp[i-1][j+dp[i-1][j]]\)

code

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,dp[60][270000],ans;
int main(){
	scanf("%d",&n);
	int x;
	for(int i=1;i<=n;i++) scanf("%d",&x),dp[x][i]=1;
	for(int i=2;i<=58;i++){
		for(int j=1;j<=n;j++){
			if(!dp[i][j]) 
				if(dp[i-1][j]&&dp[i-1][j+dp[i-1][j]])//判断两个区间是否存在 
					dp[i][j]=dp[i-1][j]+dp[i-1][j+dp[i-1][j]];
			if(dp[i][j]) ans=max(ans,i);
		}
	} 
	cout<<ans<<endl;
	return 0;
}

\(i\) 的最大值为 \(58\) 是因为 \(2^{18}=262144\) 且范围为 \(1\sim40\)

所以 \(i\) 的最大值为 \(40+18=58\)

summary

算是拓宽了在区间 \(dp\) 解法中的另一种常规思路吧。和 \(ST\) 算法有着异曲同工之妙,也算变相复习了 \(ST\) 算法了。

\(2023.1.29\)

posted @ 2023-01-29 12:12  k_stefani  阅读(36)  评论(0编辑  收藏  举报