<DP>总结
DP总结
基础DP
[USACO1.5] [IOI1994]数字三角形 Number Triangles
对于到达(i,j)点时的最大值,其状态仅由(i-1,j)和(i-1,j-1)决定。
故设计dp[ i ] [ j ] = MAX(dp[ i-1 ] [ j ],dp[ i-1 ] [ j-1 ])+num[ i ] [ j ]
#include<bits/stdc++.h>
using namespace std ;
int n ;
int num[1001][1001] ;
int dp[1001][1001] ;
int main()
{
scanf("%d",&n) ;
for(int i=1;i<=n;++i)
for(int j=1;j<=i;++j)
scanf("%d",&num[i][j]) ;
memset(dp,0,sizeof(dp)) ;
dp[1][1]=num[1][1] ;
for(int i=1;i<=n-1;++i)
{
for(int j=1;j<=i;++j)
{
if(i+1<=n&&j<=i+1)
dp[i+1][j]=max(dp[i+1][j],dp[i][j]+num[i+1][j]) ;
if(i+1<=n&&j+1<=i+1)
dp[i+1][j+1]=max(dp[i+1][j+1],dp[i][j]+num[i+1][j+1]) ;
}
}
int ans = 0 ;
for(int j=1;j<=n;++j) ans = max(ans,dp[n][j]) ;
printf("%d",ans) ;
return 0 ;
}
[NOIP1996 提高组] 挖地雷
该题为比较简单的图上DP,注意题目要求只能从编号小的地窖走向编号较大的地窖。
设计dp[i]表示从i开始往下走能获得的最大价值。
考虑从编号最大的地窖开始倒着处理,由于题目要求输出路径,在更新DP时记录路径 即可
#include<bits/stdc++.h>
using namespace std ;
int mp[21][21],num[21],dp[21],nxt[21] ;
int n ;
int maxid , maxans=-1 ;
int main()
{
scanf("%d",&n) ;
for(int i=1;i<=n;++i) scanf("%d",&num[i]) ;
for(int i=1;i<n;++i)
for(int j=i+1;j<=n;++j)
scanf("%d",&mp[i][j]) ;
memset(nxt,-1,sizeof(nxt)) ;
memset(dp,0,sizeof(dp)) ;
dp[n] = num[n] ;
for(int i=n-1;i>=1;--i)
{
dp[i]=num[i] ;
for(int j=i+1;j<=n;++j)
{
if(mp[i][j]==0) continue ;
if(dp[j]+num[i]>dp[i])
{
dp[i]=dp[j]+num[i] ;
nxt[i]=j ;
}
}
}
for(int i=1;i<=n;++i)
{
if(dp[i]>maxans)
{
maxans=dp[i] ;
maxid = i ;
}
}
for(;;)
{
printf("%d ",maxid) ;
maxid = nxt[maxid] ;
if(maxid==-1) break ;
}
printf("\n") ;
printf("%d",maxans) ;
return 0 ;
}
LuoguP1434 [SHOI2002] 滑雪
二位最长降序路径长度,常规DP。
考虑dp[ i ] [ j ]表示在该点结束时的最大路径长度,其状态可以由四周比它高的点转移得到
注意处理后效性,需要将点排序从高到底处理
#include<bits/stdc++.h>
using namespace std ;
int n,m ;
int num[101][101] ;
int dp[101][101] ;
struct point
{
int height ;
int x,y ;
}a[10001] ;
int cnt=0 ;
bool cmp (point x,point y)
{
if(x.height>y.height) return true ;
return false ;
}
int main()
{
scanf("%d%d",&n,&m) ;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;++j)
{
scanf("%d",&num[i][j]) ;
a[++cnt].height=num[i][j] ;
a[cnt].x=i ;
a[cnt].y=j ;
}
}
memset(dp,0,sizeof(dp)) ;
sort(a+1,a+1+cnt,cmp) ;
for(int i=1;i<=cnt;++i)
{
if(a[i].x-1>=1)
{
if(num[a[i].x][a[i].y]<num[a[i].x-1][a[i].y])
dp[a[i].x][a[i].y]=max(dp[a[i].x][a[i].y],dp[a[i].x-1][a[i].y]+1) ;
}
if(a[i].x+1<=n)
{
if(num[a[i].x][a[i].y]<num[a[i].x+1][a[i].y])
dp[a[i].x][a[i].y]=max(dp[a[i].x][a[i].y],dp[a[i].x+1][a[i].y]+1) ;
}
if(a[i].y-1>=1)
{
if(num[a[i].x][a[i].y]<num[a[i].x][a[i].y-1])
dp[a[i].x][a[i].y]=max(dp[a[i].x][a[i].y],dp[a[i].x][a[i].y-1]+1) ;
}
if(a[i].y+1<=m)
{
if(num[a[i].x][a[i].y]<num[a[i].x][a[i].y+1])
dp[a[i].x][a[i].y]=max(dp[a[i].x][a[i].y],dp[a[i].x][a[i].y+1]+1) ;
}
}
int ans=-1 ;
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
if(dp[i][j]>ans) ans = dp[i][j] ;
printf("%d",ans+1) ;
return 0 ;
}
P4017 最大食物链计数
这道题与滑雪相似,在图上DP,思路也一样,本质上都是通过拓扑序来满足DP的无后效性,使得先处理的点不会再更改且不会影响到后面的点。这题通过拓扑排序+BFS直接一边递推出结果
#include<bits/stdc++.h>
using namespace std ;
int n,m ;
int head[5001] ,deg[5001],cnt=0;
bool vis[5001],ans[5001] ;
int dp[5001] ;
struct edge
{
int to ;
int pre ;
}e[500001] ;
queue<int>q ;
void addEdge(int x,int y)
{
e[++cnt].to = y ;
e[cnt].pre = head[x] ;
head[x] = cnt ;
}
int main()
{
memset(ans,false,sizeof(ans)) ;
memset(vis,false,sizeof(vis)) ;
scanf("%d%d",&n,&m) ;
for(int i=1;i<=m;++i)
{
int x,y ;
scanf("%d%d",&x,&y) ;
addEdge(y,x) ;
deg[x]++ ;
ans[y]=true ;
}
for(int i=1;i<=n;++i)
{
dp[i]=0 ;
if(deg[i]==0)
{
dp[i]=1 ;
q.push(i) ;
}
}
while(q.empty()==false)
{
int x = q.front() ;
q.pop() ;
for(int i=head[x];i;i=e[i].pre)
{
int v=e[i].to ;
dp[v]=(dp[v]+dp[x])%80112002 ;
deg[v]-- ;
if(deg[v]==0) q.push(v) ;
}
}
int sum = 0 ;
for(int i=1;i<=n;++i)
{
if(ans[i]==false)
{
sum=(sum+dp[i])%80112002 ;
}
}
printf("%d",sum) ;
return 0 ;
}
背包问题
0-1背包
[NOIP2005 普及组] 采药
这是最典型的0-1背包问题,背包容量+物品价值,每个物品只能用一次。
设计dp[i]表示花费i时间所能获得的最大值。
考虑一次处理每一个物品,保证每一个物品只能用一次
然后在枚举花费时间时,当i<j时,dp[i]会影响到dp[j],正正着枚举会导致同一个物品多次放入,所以应该倒着枚举。
#include<bits/stdc++.h>
using namespace std ;
int T,m ;
int value[101],tim[101] ;
int dp[1001] ;
int main()
{
scanf("%d%d",&T,&m) ;
for(int i=1;i<=m;++i)
scanf("%d%d",&tim[i],&value[i]) ;
memset(dp,0,sizeof(dp)) ;
for(int j=1;j<=m;++j)
for(int i=T;i>=0;--i)
if(i-tim[j]>=0)
dp[i]=max(dp[i],dp[i-tim[j]]+value[j]) ;
printf("%d",dp[T]) ;
return 0 ;
}
混合背包
P1833 樱花
解题思路:
这道题是常规混合背包,直接分情况考虑,完全背包时正着dp,k重背包时反着dp。但最终会超时。
考虑对混合背包的优化,主要针对于k重背包和完全背包情况时的优化:二进制拆分
通过将k重背包的k个相同物品进行二进制拆分成logk个不同的物品来减少枚举物品数量时的复杂度:
#include<bits/stdc++.h>
using namespace std ;
char s[10],t[10] ;
int n,m,st,et,cnt=0 ;
int T[1000005],C[1000005] ;
int dp[1005] ;
void getTime()
{
int lens = strlen(s+1) ;
int lent = strlen(t+1) ;
for(int i=1;i<=lens;++i)
{
if(s[i]==':')
{
int h=0 ;int p=1 ;
for(int j=i-1;j>=1;--j)
{
h+=(s[j]-'0')*p ;
p=p*10 ;
}
st+=h*60 ;
int m=0 ;p=10 ;
for(int j=i+1;j<=lens;++j)
{
m+=(s[j]-'0')*p ;
p=p/10 ;
}
st+=m ;
break ;
}
}
for(int i=1;i<=lent;++i)
{
if(t[i]==':')
{
int h=0 ;int p=1 ;
for(int j=i-1;j>=1;--j)
{
h+=(t[j]-'0')*p ;
p=p*10 ;
}
et+=h*60 ;
int m=0 ;p=10 ;
for(int j=i+1;j<=lent;++j)
{
m+=(t[j]-'0')*p ;
p=p/10 ;
}
et+=m ;
break ;
}
}
m = et-st ;
}
int main()
{
scanf("%s",s+1) ;
scanf("%s",t+1) ;
scanf("%d",&n) ;
getTime() ;
for(int i=1;i<=n;++i)
{
int x,y,z ;
scanf("%d%d%d",&x,&y,&z) ;
if(z==0) z = 1000 ;
int P=1 ;
while(z)
{
cnt++ ;
T[cnt] = P*x ;
C[cnt] = P*y ;
z-=P ;
if(P*2>z)
{
cnt++ ;
T[cnt] = z*x ;
C[cnt] = z*y ;
break ;
}
P=P*2 ;
}
}
for(int i=1;i<=cnt;++i)
{
for(int j=m;j>=T[i];--j)
dp[j] = max(dp[j],dp[j-T[i]]+C[i]) ;
}
printf("%d",dp[m]) ;
return 0 ;
}
多维背包
P1874 快速求和
- 这道题可以直接暴力搜索加剪枝直接过
- 这道题也可以从背包问题的角度考虑dp
设计dp [ i ] [ j ] 表示前i个字符最少需要加多少个+号才能使值为j
所以这里有类似背包问题的转移方式:
dp[ i ] [ j ] = Min ( dp[ i ] [ j ] , dp[ k-1 ] [ j-num[ k ] [ i ] ]+1)
其中预处理num[ i ] [ j ] 表示 字符串 从位置i到位置j中间不加任何+号所表示的数值
#include<bits/stdc++.h>
using namespace std ;
char s[43] ;
long long n ;
long long tar ;
long long num[43][43] ;
long long dp[43][100001] ;
int main()
{
cin>>s+1 ;
n = strlen(s+1) ;
for(long long i=1;i<=n;++i)
{
num[i][i] = s[i]-'0' ;
for(long long j=i+1;j<=n&&j-i<=11;++j)
num[i][j] = num[i][j-1]*10+(s[j]-'0') ;
}
scanf("%lld",&tar) ;
if(n==0)
{
printf("0") ;
return 0 ;
}
for(long long i=0;i<=n;++i)
for(long long j=0;j<=tar;++j)
dp[i][j] = 0x7fffffff ;
dp[0][0] = 0 ;
for(long long i=1;i<=n;++i)
{
for(long long k=i;k>=1&&i-k<=11;--k)
{
for(long long j=num[k][i];j<=tar;++j)
{
dp[i][j] = min(dp[i][j],dp[k-1][j-num[k][i]]+1) ;
}
}
}
if(dp[n][tar]>=40) printf("-1") ;
else printf("%lld",dp[n][tar]-1) ;
return 0 ;
}
线性DP
P1020 [NOIP1999 普及组] 导弹拦截
问题一:最长不上升子序列
题目要求为nlogn的做法
设计状态dp[ i ] 表示以第i个为结尾的子序列的最大长度。
考虑状态转移方程:dp[ i ] = max(dp[ j ])+1 ;(其中num[ i ] < num[ j ] )
如果正常遍历则复杂度为n方,所以考虑优化
主要优化寻找最优dp[ j ],且必须满足降序的条件
引入 f [ x ] 表示长度为x的所有不上升序列的最后一个数的最大的那一个
易知:随x增大,f[ x ]不上升
对于寻找满足条件的最大的dp[ j ] 根据定义可知 num[ j ] <= f[ dp[ j ] ]
又因为降序要求 num[ j ] >= num[ i ]
所以可知num[ i ] <= f [ dp[ j ] ]
因为f[ x ]随x增大不上升,所以f[ x ]是一个有序数组,可以用二分的方法找到最大的dp[ j ] 同时还满足num[ i ] <= f [ dp[ j ] ]
这样就成功优化到了nlogn
问题二:最少用多少个不上升子序列能覆盖整个序列
Dilworth 定理 :
对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。此定理的对偶形式亦真。
这道题中不上升为偏序集的正链,严格上升则为偏序集的反链。
所以应用定理:偏序集正链的划分数等于反链的元素个数,即严格上升子序列的长度。
所以只需要再求一遍严格上升子序列的长度即可。方法与问题一类似
#include<bits/stdc++.h>
using namespace std ;
int num[500001] ;
int f[500001] ;
int n=0,ans ;
int find_down(int x)
{
int l=0,r=ans+1;
while(r-l>1)
{
int m=l+(r-l)/2;
if(f[m]>=x) l=m;
else r=m;
}
return l ;
}
int find_up(int x)
{
int l=0,r=ans+1;
while(r-l>1)
{
int m=l+(r-l)/2;
if(f[m]<x) l=m;
else r=m;
}
return l ;
}
int main()
{
while(~scanf("%d",&num[++n])) ;
--n ;
ans=0 ;
f[0] = INT_MAX ;
for(int i=1;i<=n;++i)
{
int pos = find_down(num[i]) ;
if(pos+1>ans) ans = pos+1 ;
f[pos+1] = num[i] ;
}
printf("%d\n",ans) ;
memset(f,0,sizeof(f)) ;
f[0]=0 ;
ans=0 ;
for(int i=1;i<=n;++i)
{
int pos = find_up(num[i]) ;
if(pos+1>ans) ans = pos+1 ;
f[pos+1] = num[i] ;
}
printf("%d\n",ans) ;
return 0 ;
}
P2285 [HNOI2004] 打鼹鼠
鼹鼠总范围是1000,考虑用n方的复杂度完成,由于题目已经保证了按出现时间顺序给出,所以无后效性一定满足。
设计状态 dp[ i ]表示打完第i个地鼠时打地鼠值的最大值 。
状态转移方程为 : dp[ i ] = max( 1 , dp[ j ] + 1 ) ;
其中j为满足两点曼哈顿距离小于两点地鼠出现的时间差
#include<bits/stdc++.h>
using namespace std ;
int n,m ;
struct info
{
int tim ;
int x,y ;
}a[100001] ;
int dp[100001] ;
int ans=-1 ;
int main()
{
scanf("%d%d",&n,&m) ;
for(int i=1;i<=m;++i) scanf("%d%d%d",&a[i].tim,&a[i].x,&a[i].y) ;
for(int i=1;i<=m;++i)
{
dp[i]=1 ;
for(int j=1;j<i;++j)
{
if(abs(a[i].x-a[j].x)+abs(a[i].y-a[j].y)<=abs(a[i].tim-a[j].tim))
{
dp[i] = max(dp[i],dp[j]+1) ;
}
}
ans = max(ans,dp[i]) ;
}
printf("%d",ans) ;
return 0 ;
}
P4933 大师
观察这道题的数据范围,发现n=1000,h[i]<=20000 ;发现可以用n方的算法存储,并且二维数组也能刚好存的下
设计dp状态
dp[ i ] [ j ] 表示以第i个数结尾的公差为j的所有等差数列的个数(不包括当个数字且必须以第i个数结尾!)
那么可以得到状态转移方程式:dp[ i ] [ k ] = dp[ j ] [ k ] + 1(要求j[i]-h[j]=k)
意思是所有以j结尾的公差为k的等差数列都可以向前延申到i,总数量为dp[ j ] [ k ] ;
同时从单个j直接延申到i(因为在dp定义中单个数字是不包括在dp计数范围内的),数量为1
所以dp方程式就如上式;
注意由于题目要求,我们不仅要统计dp中的数量,同时还要统计单个数字的情况
#include<bits/stdc++.h>
using namespace std ;
const int mod = 998244353 ;
const int maxdiff = 20000 ;
int n ;
int a[1001] ;
int dp[1001][40001] ;
int ans = 0 ;
int main()
{
scanf("%d",&n) ;
for(int i=1;i<=n;++i) scanf("%d",&a[i]) ;
for(int i=1;i<=n;++i)
{
ans=(ans+1)%mod ;
for(int j=1;j<i;++j)
{
dp[i][a[i]-a[j]+maxdiff]=(dp[i][a[i]-a[j]+maxdiff]+dp[j][a[i]-a[j]+maxdiff]+1)%mod ;
ans = (ans+dp[j][a[i]-a[j]+maxdiff]+1)%mod ;
}
}
printf("%d",ans) ;
return 0 ;
}
P1439 【模板】最长公共子序列
-
n方做法:
设计dp状态: dp[ i ] [ j ] 表示a序列的前i个和b序列的前j个的最长公共子序列长度
当a[ i ] = b[ j ]时可以转移:
dp[ i ] [ j ] = max ( dp[ i ] [ j ] , dp[ i-1 ] [ j-1 ] +1 ) ;
当不等于时可以考虑继承
dp[ i ] [ j ] = max ( dp[ i-1 ] [ j ] , dp[ i ] [ j-1 ] ) ;
n方传统做法
-
nlogn做法:
最长公共子序列与最长上升子序列是等价的!
证明如下:
假如我们定义一种新的大小关系,这种大小关系就是按照a数组的顺序,即a数组就是新定义中的从小到大的顺序
显然最长公共序列也是a的子序列,所以最长公共子序列也满足我们新定义的从小到大的顺序
那么该序列同时也是b的子序列,那么问题就变成了从b中以新定义的大小关系为基础,找出b中的最长上升子序列
#include<bits/stdc++.h>
using namespace std ;
int n ;
int a[100001],b[100001] ;
int pos[100001] ;
int f[100001];
int ans=0 ;
int find(int x)
{
int l=0,r=ans+1 ;
while(r-l>1)
{
int mid =l+(r-l)/2 ;
if(pos[x]>=pos[f[mid]]) l=mid;
else r=mid ;
}
return l ;
}
int main()
{
scanf("%d",&n) ;
for(int i=1;i<=n;++i)
{
scanf("%d",&a[i]) ;
pos[a[i]]=i ;
}
for(int i=1;i<=n;++i) scanf("%d",&b[i]) ;
memset(f,0,sizeof(f)) ;
for(int i=1;i<=n;++i)
{
int now = find(b[i]) ;
if(now+1>ans) ans = now + 1 ;
f[now+1] = b[i] ;
}
printf("%d",ans) ;
return 0 ;
}
P2758 编辑距离
设计dp状态 dp[ i ] [ j ] 表示将a字符串的前i个转化为b字符串的前j个的最少步数
dp[ i ] [ j ] 可以从以下三个状态转换
- dp[ i-1 ] [ j ] : 多一步删除
- dp[ i ] [ j-1 ] : 多一步添加
- dp[ i-1 ] [ j-1 ] : 如果a[i]=b[j] 则不用变化,反之则需要多一步修改
#include<bits/stdc++.h>
using namespace std ;
char A[2001] ;
char B[2001] ;
int Na,Nb ;
int dp[2001][2001] ;
int main()
{
scanf("%s",A+1) ;
scanf("%s",B+1) ;
Na = strlen(A+1) ;
Nb = strlen(B+1) ;
for(int i=0;i<=2000;++i)
{
for(int j=0;j<=2000;++j)
{
dp[i][j] = 2147483647 ;
if(i==0) dp[i][j] = j ;
if(j==0) dp[i][j] = i ;
}
}
for(int i=1;i<=Na;++i)
{
for(int j=1;j<=Nb;++j)
{
dp[i][j] = min(dp[i][j],dp[i][j-1]+1) ;
dp[i][j] = min(dp[i][j],dp[i-1][j]+1) ;
if(A[i]==B[j]) dp[i][j] = min(dp[i][j],dp[i-1][j-1]) ;
else dp[i][j] = min(dp[i][j],dp[i-1][j-1]+1) ;
}
}
printf("%d",dp[Na][Nb]) ;
return 0 ;
}
P2679 [NOIP2015 提高组] 子串
设计dp状态: dp[ i ] [ j ] [ k ] 表示用A串前i个字符顺序取k个子串正确拼接成B串的前k个
从拼接的角度考虑 k 可能从 k 转移得到,也可能从k-1转移得到
所以:
于是就有了如下版本的做法:
#include<bits/stdc++.h>
using namespace std ;
const int mod = 1000000007 ;
int n,m,K ;
char a[1001],b[201] ;
int dp[1001][201][201] ;
int main()
{
scanf("%d%d%d",&n,&m,&K) ;
scanf("%s",a+1) ;
scanf("%s",b+1) ;
for(int i=0;i<=n;++i)
dp[i][0][0]=1 ;
for(int i=1;i<=n;++i)
{
for(int j=1;j<=i&&j<=m;++j)
{
int tempi=i,tempj=j ;
while(a[tempi]==b[tempj]&&tempi>=1&&tempj>=1)
{
for(int k=1;k<=j&&k<=K;++k)
{
dp[i][j][k]=(dp[tempi-1][tempj-1][k-1]+dp[i][j][k])%mod ;
}
tempi-- ;
tempj-- ;
}
for(int k=1;k<=j&&k<=K;++k)
{
dp[i][j][k]=(dp[i][j][k]+dp[i-1][j][k])%mod ;
}
}
}
printf("%d",dp[n][m][K]) ;
return 0 ;
}
但注意到题目数据范围要求内存使用为128MB,所以该做法是爆空间的,需要进一步优化dp转移方程:
我们可以一边计算dp,一边处理前缀和的方式来降低dp状态的维度,从而减少空间使用量
其中要注意的是由于我们使用的是滚动数组,计算dp[j]时会用到dp[j-1]
但两者隐含的i维度不同,dp[j]的含义是dp[ i ] [ j ],而dp[j-1]的含义是dp[i-1] [j-1]
所以在枚举j的时候要倒序枚举!
#include<bits/stdc++.h>
using namespace std ;
const int mod = 1000000007 ;
int n,m,K ;
char a[1001],b[201] ;
int dp[201][201]={0} ;
int sum[201][201]={0} ;
int main()
{
scanf("%d%d%d",&n,&m,&K) ;
scanf("%s",a+1) ;
scanf("%s",b+1) ;
dp[0][0]=1 ;
for(int i=1;i<=n;++i)
{
for(int j=m;j>=1;--j)
{
for(int k=1;k<=j&&k<=K;++k)
{
if(a[i]==b[j])//可以沿用之前累加的
sum[j][k]=(sum[j-1][k]+dp[j-1][k-1])%mod ;//直接拼上去
//不能沿用之前的,要清零
else
sum[j][k]=0 ;
dp/*[i]*/[j][k]=(dp/*[i-1]*/[j][k]+sum[j][k])%mod ;
}
}
}
printf("%d",dp[m][K]) ;
return 0 ;
}
P1435 [IOI2000] 回文字串
设计dp状态 dp[ i ] [ j ] 表示第i个字符到第j个字符之间的字符串若要变成回文串所需添加的最小字符数
注意到回文串本身的性质:回文串中内部更小的串同样也是回文串,这样就满足了可递推性。
考虑从子串的长度方向开始枚举:
考虑状态转移:
对于任意一个串,我们需要找到其最长的回文子串,剩余的部分再对称添加即可
所以一共三种情况:
dp[ i ] [ j ]
- 从dp [ i ] [ j-1 ] 转移,此时只需要在开头添加一个 j 字母即构成回文串
- 从dp [ i+1 ] [ j ] 转移,此时只需要在末尾添加一个 i 字母即构成回文串
- 从dp[ i+1 ] [ j-1 ] 转移,如果两头字母相等则本身就是回文串,继承dp[ i+1 ] [ j-1 ],若不相等,则两头对称添加即可
#include<bits/stdc++.h>
using namespace std ;
int dp[1001][1001] ;
char str[1001] ;
int n ;
int main()
{
scanf("%s",str+1) ;
int n = strlen(str+1) ;
for(int i=1 ; i<=n;++i)
dp[i][i]=0 ;
for(int len=2;len<=n;++len)
{
for(int i=1;i+len-1<=n;++i)
{
if(len==2)
{
if(str[i]==str[i+1])dp[i][i+len-1]=0 ;
else dp[i][i+len-1]=1 ;
}
int flag = 0 ;
if(str[i]==str[i+len-1]) flag=0 ;
else flag=2 ;
dp[i][i+len-1] = min(min(dp[i][i+len-2]+1,dp[i+1][i+len-1]+1),dp[i+1][i+len-2]+flag) ;
}
}
printf("%d",dp[1][n]) ;
return 0 ;
}
P4310 绝世好题
解题思路:正常dp是n方的,而且不满足单调性,无法用单调队列优化。
考虑转移情况,只有与当前数二进制位中有相同位置为1的数才能转移至当前状态。
所以考虑枚举当前数的所有为一的位。
#include<bits/stdc++.h>
using namespace std ;
int n ;
int a[100001] ;
int dp[100001],bit[35] ;
int ans ;
int lowbit(int x)
{
return x&(-x) ;
}
int main()
{
scanf("%d",&n) ;
for(int i=1;i<=n;++i) scanf("%d",&a[i]) ;
for(int i=1;i<=n;++i)
{
int num = a[i] ;
while(num)
{
int x = lowbit(num) ;
num-=x ;
int pos = log2(x) ;
dp[i] = max(dp[i],bit[pos]+1) ;
}
num = a[i] ;
while(num)
{
int x = lowbit(num) ;
num-=x ;
int pos = log2(x) ;
bit[pos] = max(bit[pos],dp[i]) ;
}
ans = max(ans,dp[i]) ;
}
printf("%d",ans) ;
return 0 ;
}
区间DP
状压DP
树型DP
数位DP
DP优化
单调队列优化DP
单调队列:单调队列优化动态规划问题的基本形态:当前状态的所有值可以从上一个状态的某个连续的段的值得到,要对这个连续的段进行 RMQ 操作,相邻状态的段的左右区间满足非降的关系。
基本模型:
l=1;r=0 ;
for(int i=1;i<=n;++i)
{
while(l<=r&&q[l]+k-1<i) l++ ;//不在有效范围内的出队
while(l<=r&&a[i]<=a[q[r]]) r-- ;//比新入队的差的出队
q[++r]=i ;//新的入队
if(i>=k) printf("%d ",a[q[l]]) ;//当前对头为当前最优解
}
使用单调队列优化时,可以先将dp状态转移方程是变换形式,使其与之前一段的区间有关系
P1725 琪露诺
设计dp [ i ]为当前到第i格后已经获取的最大值
不难得出状态转移方程 dp[ i ] = a[ i ] + MAX( dp[ j ] ) ;其中j+L<= i <= j+R
而这个个状态转移方程时近似n方的,我们需要把他优化。
注意状态转移方程是和某一段连续区间的最大值有关,所以考虑用单调队列优化
#include<bits/stdc++.h>
using namespace std ;
int n,L,R,ans=INT_MIN ;
int a[200001] ;
int dp[200001] ;
int l=1,r=0 ;
int q[200001] ;
void insert(int x,int now)
{
while(l<=r&&q[l]+R<now) l++ ;
while(l<=r&&dp[x]>=dp[q[r]]) r-- ;
q[++r] = x ;
}
int main()
{
scanf("%d%d%d",&n,&L,&R) ;
for(int i=0;i<=n;++i)
{
scanf("%d",&a[i]) ;
dp[i] = INT_MIN ;
}
dp[0] = 0 ;
for(int i=L;i<=n;++i)
{
insert(i-L,i) ;
dp[i] = dp[q[l]]+a[i] ;
if(i+R>n) ans = max(ans,dp[i]) ;
}
printf("%d",ans) ;
return 0 ;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!