<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 快速求和

  1. 这道题可以直接暴力搜索加剪枝直接过
  2. 这道题也可以从背包问题的角度考虑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 【模板】最长公共子序列

  1. 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方传统做法

  2. 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 ] 可以从以下三个状态转换

  1. dp[ i-1 ] [ j ] : 多一步删除
  2. dp[ i ] [ j-1 ] : 多一步添加
  3. 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 ]

  1. 从dp [ i ] [ j-1 ] 转移,此时只需要在开头添加一个 j 字母即构成回文串
  2. 从dp [ i+1 ] [ j ] 转移,此时只需要在末尾添加一个 i 字母即构成回文串
  3. 从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 ;
}
posted @   Bo-Wing  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示