8月25-27日集训小记
前言
最近被h3z拉去集训三天,效率高的离谱,做了很多DP,故在这里写篇博客记录一下。
8月25日
下午机房里dyx与lyd激情研究数据结构,在黑板上留下了“轻舟已过万重山 lyd小朋友领跑全机房”的惊人字样,至今未擦除(截止8月26日)。
测试
上午考一场开学测,T1没开long long痛失65分,T2太贪痛失97分,T4没做完。
T1
简单模拟题,打表可注意到长度为n的数列中,第1个数出现了1*n次,第2个数出现了2*(n-1)次,以此上升直至中间的顶峰再同样下降至1*n次
必须开long long,加和的时候需要模数
T2
考场想了一个LIS贪心,挂了。赛后被小升给了一个hack数据,想出了正解。也不算难。
先正序遍历一遍,找出从1到i的最大值存在数组maxx[i]中;再倒序遍历一遍,找出从n到i的最小值存在minx[i]中,如果maxx[i]<minx[i+1]则可以分一组
布丁老师的解法很逆天,没学会(
T3
二分查找。
笑点解析:考前,大概是7月中旬吧,练了大量二分。导致这场考试只有二分题切掉了。
所有住户的范围可以表示成一个线段,目标是找出所有没有交点的线段对。
根据线段的首端点升序排列,如果线段B的首端点大于线段A的尾端点,那么线段B及其之后的所有线段都能和A构成一对没有交点的线段对。只需要二分查找出第一个大于线段A尾端点的首端点所在的线段,再计算累积一下即可。
T4
话说这是我第一次见到后缀和(其实就是前缀和反过来)
删去一个数,这个数前面的走法和原来一样,后面的走法正好是原来的错开一位。
维护两个数组,前缀和s与后缀和v。计算方法和原来稍有不同,隔一位相加。
根据所删去数字下标的奇偶性判断前缀和与后缀和的计算方法即可。题目不难,就是打表恶心。
(赛时开了八个数组记录前缀和与后缀和,推的式子死长死长,最后还没做完。)
T5
不开玩笑,T5可能是这场比赛最难的题?贪心+二分。
一眼二分答案。验证答案时,所有牛都尽量往左靠,看绳长是否大于目标答案,以及牛的总长度是否大于农场总长度。二分答案即可。
难点主要在贪心的模拟实现上。有点类似跳石头。
练习
1. P2858 [USACO06FEB] Treats for the Cows G/S
题意简述:有一个双端队列,里面的元素已知,现在将里面的元素取出(可以从队头,也可以从队尾),取出元素对答案的贡献是元素值乘以取出的顺序号。求一种取出方案,使得答案最大。
f[l][r]表示从第l个零食到第r个零食所能取出的最大价值
典型区间DP,故遍历i和l,i表示区间长度。对于区间[l,r],只有取出队头或队尾元素两种操作。
转移方程是f[l][r]=max(f[l][r-1]+v[r]*(n-i+1),f[l+1][r]+v[l]*(n-i+1))
对于自己一个元素,最大的价值产生,当且仅当它总是最后一个被取出,所以边界是f[i][i]=v[i]*n
code:
#include <bits/stdc++.h>
using namespace std;
const int N=2010;
int n,f[N][N],v[N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>v[i];
f[i][i]=v[i]*n;
}
for(int i=2;i<=n;i++){
for(int l=1;l<=n;l++){
int r=l+i-1;
f[l][r]=max(f[l][r-1]+v[r]*(n-i+1),f[l+1][r]+v[l]*(n-i+1));
}
}
cout<<f[1][n];
return 0;
}
2. P1564 膜拜
线性DP+前缀和。
题目说的挺明白的。
不妨令膜拜甲的人对答案的贡献为1,膜拜乙的人对答案的贡献为-1,由于计算的是一段数列的和,可以使用前缀和数组s维护。
定义f[i]表示前i个人最少需要的机房数。
显然f[i]=min(f[i],f[j]+1),其中1<=j<=i,且序列j~i可以分到一个机房
不难看出,当且仅当abs(s[j]-s[i-1])==i-j+1 || abs(s[j]-s[i-1])<=m时,才能转移状态
code:
#include <bits/stdc++.h>
using namespace std;
const int N=2510;
int n,m,s[N],f[N];
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int x;
cin>>x;
if(x==1){
s[i]=s[i-1]+1;
}else{
s[i]=s[i-1]-1;
}
}
memset(f,0x3f,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
int con=abs(s[i]-s[j-1]);
if(con==i-j+1 || con<=m){
f[i]=min(f[i],f[j-1]+1);
}
}
}
cout<<f[n];
return 0;
}
8月26日
H同学由于之前搞开发的原因,一直习惯数组下标从0开,导致他做“跳石头”时,没设左边界,WA了9个点,改了半天没改过来。
L同学下午三点左右头痛突发,用了三次WC大法,还是没缓过来。
L同学在解决一道高精问题时写出了#define one 1的惊人语句。
练习
1. P1103 书本整理
线性DP。题干中的“高度”就是用来排序的,不参与状态计算。
思维转换:从中去掉k本,即留下n-k本,所以先k=n-k;
f[i][l]表示前i本书留下l本获得的最小不整齐度
边界:f[i][1]=0
转移:f[i][l]=min(f[i][l],f[j][l-1]+abs(a[i].width-a[j].width))
其中i从1到n,j从1到i-1,l从1到min(i,k)(否则不够取)
答案则是f[i][k]的最小值,其中i从k到n
#include <bits/stdc++.h>
using namespace std;
const int N=110;
int n,k,f[N][N];
struct Node{
int height,width;
}a[N];
bool cmp(Node x,Node y){
return x.height>y.height;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
memset(f,0x3f,sizeof f);
cin>>n>>k;
k=n-k;
for(int i=1;i<=n;i++){
cin>>a[i].height>>a[i].width;
}
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++){
f[i][1]=0;
}
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
for(int l=1;l<=min(i,k);l++){
f[i][l]=min(f[i][l],f[j][l-1]+abs(a[i].width-a[j].width));
}
}
}
int ans=INT_MAX;
for(int i=k;i<=n;i++){
ans=min(ans,f[i][k]);
}
cout<<ans;
return 0;
}
2. P1006 [NOIP 2008 提高组] 传纸条
类似P1004 [NOIP 2000 提高组] 方格取数,几乎是一模一样。区别是这道题不能取已取过的数。
还是用四位数组定义状态,存两个坐标,分别表示第一回和第二回到达的点的坐标。顺序转移即可,加个去重特判就AC了。
这题能降维,但我没试,有时间研究研究。据说还能用费用流做。至于费用流是个啥,我也没学。还是太弱了。
code:
#include <bits/stdc++.h>
using namespace std;
const int N=55;
int n,m,a[N][N],f[N][N][N][N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
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++){
for(int k=1;k<=n;k++){
for(int l=1;l<=m;l++){
f[i][j][k][l]=max(max(f[i-1][j][k-1][l],f[i][j-1][k-1][l]),max(f[i][j-1][k][l-1],f[i-1][j][k][l-1]))+a[i][j]+a[k][l];
if(i==k && j==l && !(i==1 && j==1 || i==n && j==m)){
f[i][j][k][l]=INT_MIN;
}
}
}
}
}
cout<<f[n][m][n][m];
return 0;
}
3. P1043 [NOIP 2003 普及组] 数字游戏
环形DP。
这类问题一般处理的办法是:把环复制一遍,变成一串长度为原来2倍的表,这样,环上所有能取到的序列就都能在这个表里取到。值得注意的是,在C++中,负数模负数仍为负数,不符合数学定义,所以我们将所有题中环上的数字都加上一个偏移量delta,使得它们都变为正数,方便取模操作。根据题意,delta应为1e4+10。这个技巧非常重要,常在数组需要以负数为下标时出现。
定义f[l][r][k]表示区间[l,r]被分成k部分,所能得到的最小值。
当区间只有一个部分时,转移方程是f[l][r][1]=min(f[l][r][1],(f[l][k][1]+f[k+1][r][1]+delta)%10),其中k属于[l,r)。
当区间有多个部分时,需要枚举区间部分数,设区间部分总数为i,则i属于[2,m],j属于[1,i)。
转移方程是f[l][r][i]=min(f[l][r][i],f[l][k][j]*f[k+1][r][i-j]),当且仅当f[l][k][j]与f[k+1][r][i-j]均被运算过。
边界显然是f[i][i][1]=(a[i]+delta)%10
最大值数组g同理。
答案为f[i][i+n-1][m]中的最值,其中i属于[1,n]。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=110,M=10,delta=1e4+10;
int n,m,a[N],f[N][N][M],g[N][N][M];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
memset(f,0x3f,sizeof f);
memset(g,0xc0,sizeof g);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
f[i][i][1]=g[i][i][1]=(a[i]+delta)%10;
}
for(int i=n+1;i<=2*n-1;i++){
a[i]=a[i-n];
f[i][i][1]=g[i][i][1]=(a[i]+delta)%10;
}
for(int len=1;len<=n;len++){
for(int l=1;l+len-1<=2*n-1;l++){
int r=l+len-1;
for(int k=l;k<r;k++){
f[l][r][1]=min(f[l][r][1],(f[l][k][1]+f[k+1][r][1]+delta)%10);
g[l][r][1]=max(g[l][r][1],(g[l][k][1]+g[k+1][r][1]+delta)%10);
for(int i=2;i<=m;i++){//区间被分割的块数
for(int j=1;j<i;j++){//断点
if(f[l][k][j]<INT_MAX && f[k+1][r][i-j]<INT_MAX)
f[l][r][i]=min(f[l][r][i],f[l][k][j]*f[k+1][r][i-j]);
if(g[l][k][j]>INT_MIN && g[k+1][r][i-j]>INT_MIN)
g[l][r][i]=max(g[l][r][i],g[l][k][j]*g[k+1][r][i-j]);
}
}
}
}
}
int minn=INT_MAX,maxn=INT_MIN;
for(int i=1;i<=n;i++){
minn=min(minn,f[i][i+n-1][m]);
maxn=max(maxn,g[i][i+n-1][m]);
}
cout<<minn<<endl<<maxn;
return 0;
}
4. P3205 [HNOI2010] 合唱队
区间DP。
对于每个人,总有两种操作,往左放或往右放。
定义f[i][j][0/1]表示区间i~j中,第i个人从左边进来/第j个人从右边进来的方案数。对于给定的a,f是可以动规的,类似于逆向推导f的值。
根据题意,转移方程如下,在这里不写了。
边界为f[i][i][0]=1(假定最开始所有人都是从左边转移过来的)
code:
#include <bits/stdc++.h>
using namespace std;
const int N=1010,mod=19650827;
int n,a[N],f[N][N][2];
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];
f[i][i][0]=1;
}
for(int len=1;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
if(a[i]<a[i+1]){
f[i][j][0]=(f[i][j][0]+f[i+1][j][0])%mod;
}
if(a[i]<a[j]){
f[i][j][0]=(f[i][j][0]+f[i+1][j][1])%mod;
}
if(a[j]>a[i]){
f[i][j][1]=(f[i][j][1]+f[i][j-1][0])%mod;
}
if(a[j]>a[j-1]){
f[i][j][1]=(f[i][j][1]+f[i][j-1][1])%mod;
}
}
}
cout<<(f[1][n][0]+f[1][n][1])%mod;
return 0;
}
5. P4342 [IOI 1998] Polygon
可能由于这题是上古IOI原题,输入方式比较特殊,必须用scanf。看了题解才搞明白。
环形DP。
对于每两个相邻的数字而言,运算符op是固定的。
状态定义f[i][j]表示区间[i,j]内操作可得的最大值。
加法:显然有f[l][r]=max(f[l][r],f[l][k]+f[k+1][r])。
乘法需要特殊考虑,因为这里有负数,干扰了最大值的判断。所以增加一个数组g维护区间内操作可得的最小值,那么加法就要补上g[l][r]=min(g[l][r],g[l][k]+g[k+1][r])。
乘法就有四种情况:最大×最大、最小×最大、最小×最小、最大×最小,必须分别考虑。
所以f的转移方程是f[l][r]=max(f[l][r],max(max(f[l][k]*f[k+1][r],g[l][k]*g[k+1][r]),max(f[l][k]*g[k+1][r],g[l][k]*f[k+1][r])))(g的转移方程同理,不再阐述)。
第一问显然是f[i][i+n-1]的最大值。而第二问则是当f[i][i+n-1]等于第一问答案时,所有数组索引的升序排列,用一个数组维护即可。
code:
#include <bits/stdc++.h>
using namespace std;
constexpr int N=110;
int ans=INT_MIN,n,a[N],f[N][N],g[N][N];//最大值 最小值
int op[N];
signed main(){
memset(f,0xc0,sizeof(f));
memset(g,0x3f,sizeof(g));
scanf("%d\n",&n);
for(int i=1;i<=n;i++){
scanf("%c %d ",&op[i],&a[i]);
op[i+n]=op[i];
a[i+n]=a[i];
}
for(int i=1;i<=n*2-1;i++){
f[i][i]=g[i][i]=a[i];
}
for(int len=2;len<=n*2-1;len++){
for(int l=1;l+len-1<=n*2-1;l++){
int r=l+len-1;
for(int k=l;k<r;k++){
if(op[k+1]=='t'){
f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]);
g[l][r]=min(g[l][r],g[l][k]+g[k+1][r]);
}else{
f[l][r]=max(f[l][r],max(max(f[l][k]*f[k+1][r],g[l][k]*g[k+1][r]),max(f[l][k]*g[k+1][r],g[l][k]*f[k+1][r])));
g[l][r]=min(g[l][r],min(min(f[l][k]*f[k+1][r],g[l][k]*g[k+1][r]),min(f[l][k]*g[k+1][r],g[l][k]*f[k+1][r])));
}
}
}
}
vector<int>s;
for(int i=1;i<=n;i++){
if(f[i][i+n-1]>ans){
ans=f[i][i+n-1];
s.clear();
s.push_back(i);
}else if(f[i][i+n-1]==ans){
s.push_back(i);
}
}
printf("%d\n",ans);
for(int i=0;i<s.size();i++){
printf("%d ",s[i]);
}
return 0;
}
6. P4170 [CQOI2007] 涂色
区间DP。
逆向思考,定义在区间[i,j]内,从给定答案涂成同色所需的最少涂色次数为f[i][j]。
可以将序列分成两部分,这两部分分别涂成同色,如果这两部分所涂颜色相同,就不需要再涂一次,否则还需要再涂一次,所以最后加上判断语句。
转移方程是f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+(s[i]!=s[j]))。
边界是f[i][i]=0,答案需要把f[1][n]加上1,因为最后是同色,还需要再涂一次。
code:
#include <bits/stdc++.h>
using namespace std;
const int N=55;
string s;
int n,f[N][N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
memset(f,0x3f,sizeof f);
cin>>s;
s=' '+s;
n=s.length()-1;
for(int i=1;i<=n;i++){
f[i][i]=0;
}
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
for(int k=i;k<j;k++){
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+(s[i]!=s[j]));
}
}
}
cout<<f[1][n]+1;
return 0;
}
7. CF607B Zuma
典型区间DP,定义f[l][r]为区间l到r的最少操作次数
显然边界f[i][i]=1,f[i][i+1]=1+(a[i]!=a[i+1])
当回文时,f[i][j]=f[i+1][j-1]
区间断点k,有f[l][r]=min(f[l][r],f[l][k]+f[k+1][n])
答案为f[1][n]
code:
#include <bits/stdc++.h>
using namespace std;
const int N=505;
int n,a[N],f[N][N];
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];
}
memset(f,0x3f,sizeof f);
for(int i=1;i<=n;i++){
f[i][i]=1;
}
for(int i=1;i<n;i++){
f[i][i+1]=1+(a[i]!=a[i+1]);
}
for(int len=3;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
if(a[l]==a[r]){
f[l][r]=f[l+1][r-1];
}
for(int k=l;k<r;k++){
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
}
}
}
cout<<f[1][n];
return 0;
}
8. P4290 [HAOI2008] 玩具取名
乍看是一棵树,其实是区间DP。这题当中用了比较少见的布尔DP数组。补充题目:262144也用了这一招。
f[i][j][c]表示原串的子串[i,j]能否被字符c替换。
边界:f[i][i][s[i]]=1(自己肯定能替换自己)。
将子串[i,j]拆分成两部分,如果这两部分都能分别替换自己的字母且这两个字母能被替换成一个字母,那么这两部分合起来的部分也能被替换成一个字母,即f[i][j][c]=f[i][k][x] && f[k+1][j][y] && vis[x][y][c]。
值得注意的是,不能直接这样赋值,得当等号右侧的值为真时再赋值,否则不变,因为可能有其他方法能转移到等号左边,换句话说,DAG指向等号左边那个节点的边有多条。
其中vis[x][y][c]表示:如果字符x和y可以合成字符c,返回真,否则为假。
code:
#include <bits/stdc++.h>
using namespace std;
int t(char c){
switch(c){
case 'W':return 1;
case 'I':return 2;
case 'N':return 3;
case 'G':return 4;
}
}
constexpr int NN=210;
int cnt[5],n;
bool f[NN][NN][10],vis[10][10][10];
string s;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
for(int i=1;i<=4;i++){
cin>>cnt[i];
}
for(int i=1;i<=4;i++){
for(int j=1;j<=cnt[i];j++){
char a,b;
cin>>a>>b;
vis[t(a)][t(b)][i]=1;
}
}
cin>>s;
s=' '+s;
n=s.length()-1;
for(int i=1;i<=n;i++){
f[i][i][t(s[i])]=1;
}
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
for(int k=i;k<j;k++){
for(int c=1;c<=4;c++){
for(int x=1;x<=4;x++){
for(int y=1;y<=4;y++){
if(f[i][k][x] && f[k+1][j][y] && vis[x][y][c]){
f[i][j][c]=1;
}
}
}
}
}
}
}
if(f[1][n][1]){
cout<<'W';
}
if(f[1][n][2]){
cout<<'I';
}
if(f[1][n][3]){
cout<<'N';
}
if(f[1][n][4]){
cout<<'G';
}
if(!(f[1][n][1]||f[1][n][2]||f[1][n][3]||f[1][n][4])){
cout<<"The name is wrong!";
}
return 0;
}
9. P1005 [NOIP 2007 提高组] 矩阵取数游戏
区间DP+高精。
注意到每一行的取数规则都是独立的,互不干扰,所以可以枚举每一行,使得这一行的取数规则最优化。
状态转移方程是显然的,不多赘述。
这题如果不想写高精板子,可以用__int128类型。注意,这种类型只支持基础运算(包括位移,但没有幂运算),不能用流输入输出,必须用类似快读快写的方法输入输出。额外地,某个常数默认为int类型,需要强转成__int128类型。
code:
#include <bits/stdc++.h>
#define int __int128
using namespace std;
int read(){
int x=0,f=1;
char c=getchar();
while(!isdigit(c)){
if(c=='-')f=-1;
c=getchar();
}
while(isdigit(c)){
x=x*10+(c^48);
c=getchar();
}
return x*f;
}
void write(int x){
if(x<0)putchar('-'),x=-x;
if(x>9)write(x/10);
putchar(x%10^48);
}
constexpr int N=90,one=1;
int m,n,a[N],f[N][N],ans;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
m=read(),n=read();
for(;m--;){
for(int i=1;i<=n;i++){
a[i]=read();
f[i][i]=(one<<n)*a[i];
}
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1,cost=one<<n-len+1;
f[i][j]=max(f[i][j-1]+cost*a[j],f[i+1][j]+cost*a[i]);
}
}
ans+=f[1][n];
}
write(ans);
return 0;
}
10. P4302 [SCOI2003] 字符串折叠
区间DP。
先写一个函数cnt返回数x的位数,再写一个函数check判断某区间以某步长为循环节长度是否能压缩。
状态定义不说了,就是根据答案。
通常情况下,f[i][j]=min(f[i][j],f[i][k]+f[k+1][j])。
当序列能被压缩时,f[i][j]=min(f[i][j],cnt(len/slen)+f[i][k]+2),当且仅当len%slen==0,其中len为序列长度,slen为循环节长度。
code:
#include <bits/stdc++.h>
using namespace std;
constexpr int N=110;
string s;
int n,f[N][N];
bool check(int l,int r,int len){
for(int i=l;i<=l+len-1;i++){
for(int j=i;j<=r;j+=len){
if(s[j]!=s[i])return 0;
}
}
return 1;
}
int cnt(int x){
if(x<=9)return 1;
if(x>=10 && x<=99)return 2;
return 3;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
memset(f,0x3f,sizeof f);
cin>>s;
s=' '+s;
n=s.length()-1;
for(int i=1;i<=n;i++){
f[i][i]=1;
}
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
for(int k=i;k<j;k++){
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
int slen=k-i+1;
if(len%slen==0 && check(i,j,slen)){
f[i][j]=min(f[i][j],cnt(len/slen)+f[i][k]+2);
}
}
}
}
cout<<f[1][n];
return 0;
}
11. P2890 [USACO07OPEN] Cheapest Palindrome G
典型区间DP。
定义f[i][j]为将区间[i,j]改为回文串的最小花费。
假设区间[i+1,j]已经改成回文串了,显然[i+1,j]左边的第i个字符,可以将其在左边删去或在右边添加,就产生了两种决策。右侧同理。
状态转移方程是f[i][j]=min(f[i+1][j]+min(add[s[i]],del[s[i]]),f[i][j-1]+min(add[s[j]],del[s[j]]))
特别地,当区间左右端字符相同时,直接转移即可,f[i][j]=min(f[i][j],f[i+1][j-1]),当且仅当区间长度不为2。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=30,M=2e3+10;
int n,m,add[N],del[N],f[M][M];
string s;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
cin>>s;
s=' '+s;
for(int i=1;i<=n;i++){
char c;
int x,y;
cin>>c>>x>>y;
add[c-'a']=x,del[c-'a']=y;
}
memset(f,0x3f,sizeof f);
for(int i=1;i<=m;i++){
f[i][i]=0;
}
for(int len=2;len<=m;len++){
for(int i=1;i+len-1<=m;i++){
int j=i+len-1;
f[i][j]=min(f[i+1][j]+min(add[s[i]-'a'],del[s[i]-'a']),f[i][j-1]+min(add[s[j]-'a'],del[s[j]-'a']));
if(s[i]==s[j]){
if(len==2)f[i][j]=0;
else
f[i][j]=min(f[i][j],f[i+1][j-1]);
}
}
}
cout<<f[1][m];
return 0;
}
12. P7074 [CSP-J2020] 方格取数
二维DP,挺好玩的一个三维状态转移,即分层图。这题做法很多,提供一种多维做法。
f[i][j][k]表示点(i,j)从上或下转移而来的最优解,0:由上至下,1:由下至上。
显然从上转移来的:f[i][j][0]=max(f[i-1][j][0]+a[i][j])
从下转移来的:f[i][j][1]=max(f[i+1][j][1]+a[i][j])(i需倒序遍历)
从左转移来的:f[i][j][0/1]=max(f[i][j-1][0/1]+a[i][j])
边界为f[1][1][0]=f[1][1][1]=a[1][1]
第一部在纵轴上只能往下走,所以还有一个边界,是f[i][1][0]=f[i-1][1][0]+a[i][1]
注意循环边界是1还是2。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=1010;
int n,m,a[N][N],f[N][N][2];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
memset(f,0xc0,sizeof(f));
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
f[1][1][0]=f[1][1][1]=a[1][1];
for(int i=2;i<=n;i++){
f[i][1][0]=f[i-1][1][0]+a[i][1];
}
for(int j=2;j<=m;j++){
for(int i=1;i<=n;i++){
f[i][j][0]=f[i][j][1]=max(f[i][j-1][0],f[i][j-1][1])+a[i][j];
}
for(int i=2;i<=n;i++){
f[i][j][0]=max(f[i][j][0],f[i-1][j][0]+a[i][j]);
}
for(int i=n-1;i>=1;i--){
f[i][j][1]=max(f[i][j][1],f[i+1][j][1]+a[i][j]);
}
}
cout<<max(f[n][m][0],f[n][m][1]);
return 0;
}

浙公网安备 33010602011771号