ACM动态规划模板(更新ing...)

  • 最长上升子序列问题
  • 循环数组最大子段和问题
  • 正整数分组问题
  • 多重背包问题
  • 多重部分和问题
  • 划分数问题
  • 多重集组合数问题
  • 最大子矩阵和问题
  • 区间DP问题
  • 数位dp问题

1、最长上升子序列问题

题目: 有一个长为n的数列a0,a1,…,an-1。请求出这个序列中最长的上升子序列的长度。上升子序列指的是对于任意的 i< j 都满足ai< aj 的子序列。
思路: 定义dp[i]为长度为i+1的上升子序列中末尾元素的最小值(不存在的话为INF)

//最长上升子序列问题
int dp[Max_n];

void solve(){
    memset(dp,0x3f,sizeof(dp));
    for(int i=0;i<n;i++)
        *lower_bound(dp,dp+n,a[i])=a[i];
    printf("%d\n",lower_bound(dp,dp+n,inf)-dp);
}

2、循环数组最大子段和问题

题目: N个整数组成的循环序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的连续的子段和的最大值。当所给的整数均为负数时和为0。(2 ≤N ≤ 50000,-10^9 ≤ a[i]≤10^9)
思路: 分情况讨论:1. 最优的最大字段和在中间部分。2.最优的最大字段和在首尾两端,此时中间部分是个最小字段和,用sum-中间部分最小字段和即可得到。

//循环数组最大子段和问题
int n;
int a[Max_n];
ll sum=0,Max=0,Min=0;

void solve(){
    ll t1=0,t2=0;
    for(int i=0;i<n;i++){
        if(t1>0)t1+=a[i];
        else t1=a[i];
        if(t1>Max)Max=t1;

        if(t2<0)t2+=a[i];
        else t2=a[i];
        if(t2<Min)Min=t2;
    }
    printf("%I64d\n",max(Max,sum-Min));
}

3、正整数分组问题

题目: 将一堆正整数分为2组,要求2组的和相差最小。例如:1 2 3 4 5,将1 2 4分为1组,3 5分为1组,两组和相差1,是所有方案中相差最少的。(N ≤100, 所有正整数的和≤10000)
思路: 重量和价值都相等的01背包变形。定义dp[i][j]表示为从前i个数中,总和不超过j的最大值。

//正整数分组问题
int n,s[110];
int dp[10010];

void solve(){
	memset(dp,0,sizeof(dp));
	    for(int i=1;i<=n;i++)
	        for(int j=sum/2;j>=s[i];j--)
	            dp[j]=max(dp[j],dp[j-s[i]]+s[i]);
	    printf("%d\n",sum-2*dp[sum/2]);
}

4、多重背包问题

题目: 有n种重量、价值和数量分别为wi,vi,ci的物品,从这些物品中挑选出总重量不超过W的物品,求出挑选物品价值总和的最大值。(1≤n≤100,1≤W≤50000)
**思路:**二进制优化多重背包。

//多重背包问题
int n,W;
int w[Max_n],v[Max_n],c[Max_n]; //重量、价值和数量
int dp[Max_W];

void ZeroOne_Pack(int w,int v){ 
    for(int i=W;i>=w;i--)
        dp[i]=max(dp[i],dp[i-w]+v);
}
void Complete_Pack(int w,int v){ 
    for(int i=w;i<=W;i++)
        dp[i]=max(dp[i],dp[i-w]+v);
}
int  Multi_Pack(){ 
    memset(dp,0,sizeof(dp));
    for(int i=0;i<n;i++){
        if(w[i]*c[i]>=W)Complete_Pack(w[i],v[i]);
        else {
            int k=1;
            while(k<c[i]){
                ZeroOne_Pack(w[i]*k,v[i]*k);
                c[i]-=k;
                k<<=1;
            }
            ZeroOne_Pack(w[i]*c[i],v[i]*c[i]);
        }
    }
    return dp[W];
}

5、多重部分和问题

题目: 有n种不同大小的数字ai,每种各ci个,判断是否可以从这些数字之中选出若干使它们的和恰好为k。(1 ≤n≤100 , 1≤K≤100000)
思路: 定义dp[i+1][j]为前 i 种数加和得到 j 时第 i 种数最多能剩余多少(不能加和得到 i 的情况为 -1)

//多重部分和问题
int n,k;
int a[Max_n],c[Max_n];
int dp[Max_k];

bool solve(){
    memset(dp,-1,sizeof(dp));
    for(int i=0;i<n;i++){
        dp[0]=c[i];
        for(int j=1;j<=k;j++){ //递推关系
            if(dp[j]>=0)dp[j]=c[i];
            else if(j>=a[i]&&dp[j-a[i]]>0)dp[j]=dp[j-a[i]]-1;
            else dp[j]=-1;
        }
    }
    if(dp[k]>=0)return true;
    else return false;
}

6、划分数问题

题目: 有n个无区别的物品,将它们划分成不超过m组,求出划分方法数模M的余数。(1 ≤m≤n≤1000)
思路: 考虑n的m划分ai(i=1,2,3…),如果对于每个ai>0,那么{ai-1}就对应了n-m的m划分,另外如果存在ai=0,那么就对应了n的m-1划分。定义dp[i][j]为 i 的 j 划分的总数,则dp[i][j]=dp[i][j-1]+dp[i-j][j]。

//划分数问题
int n,m;
int dp[Max_n][Max_m];

void solve(){
    memset(dp,0,sizeof(dp));
    dp[0][0]=1;
    for(int i=0;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(i>=j)dp[i][j]=(dp[i][j-1]+dp[i-j][j])%M;
            else dp[i][j]=dp[i][j-1];
        }
    }
    printf("%d\n",dp[n][m]);
}

7、多重集组合数问题

题目: 有n种物品,第i种物品有ai个。不同种类的物品可以互相区分但相同种类的无法区分。从这些物品中取出m个的话,有多少种取法?求出方案数模M的余数。(1≤n≤1000,1≤m≤1000,1≤ai≤1000,2≤M≤10000)
思路: 定义dp[i][j]为从前i中物品中取出j个的组合总数。

  1. 当j≤a[i]时,dp[i][j]=dp[i-1][j]+dp[i-1][j] 2. 当j>s[i]时,dp[i][j]=dp[i-1][j]+dp[i-1][j]-dp[i-1][j-1-a[i]]。
//多重集组合数问题
int n,m;
int dp[2][Max_n];

void solve(){
	memset(dp,0,sizeof(dp));
    dp[0][0]=dp[1][0]=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(j<=a[i])dp[i&1][j]=(dp[i&1][j-1]+dp[(i-1)&1][j])%mod;
            else dp[i&1][j]=(dp[i&1][j-1]+dp[(i-1)&1][j]-dp[(i-1)&1][j-1-a[i]]+mod)%mod;
            //在有取余的情况下,要避免减法运算的结果出现负数
        }
    }
    printf("%d\n",dp[n&1][m]);
}

8、最大子矩阵和问题

题目: 一个N*M的矩阵,找到此矩阵的一个子矩阵,并且这个子矩阵的元素的和是最大的,输出这个最大的值。如果所有数都是负数,就输出0。(2 <= N,M <= 500)
思路: 最后的子矩阵一定在某两行之间,枚举所有1<=i<=j<=N,表示最终子矩阵选取的行范围。分别求出第i行到第j行之间的每一列的和,第i行到第j行之间的最大子矩阵和对应于这个和数组的最大子段和。

//最大子矩阵和问题
int n,m;
int sum[Max_m];
int map[Max_n][Max_m];

void solve(){
    int Max=0;
    for(int i=1;i<=n;i++){
        memset(sum,0,sizeof(sum));
        for(int j=i;j<=n;j++){
            for(int k=1;k<=m;k++)sum[k]+=map[j][k];
            int ans=0;
            for(int k=1;k<=m;k++){ //求子矩阵的最大字段和
                if(ans>=0)ans+=sum[k];
                else ans=sum[k];
                if(ans>Max)Max=ans;
            }
        }
    }
    printf("%d\n",Max);
}

9、区间DP问题

[题目]:

n堆石子摆成一条线。现要将石子有次序地合并成一堆,规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的代价。计算将n堆石子合并成一堆的最小代价。

[普通解法]:

状态转移方程: f ( i , j ) = m i n { f ( i , k ) + f ( k + 1 , j ) } + w ( i , j ) f(i,j)=min\{f(i,k)+f(k+1,j)\}+w(i,j) f(i,j)=min{f(i,k)+f(k+1,j)}+w(i,j)
f ( i , j ) f(i,j) f(i,j)表示区间 [ i , j ] [i,j] [i,j]上的最优值, w ( i , j ) w(i,j) w(i,j)表示转移时付出的代价。

复杂度:O(n3)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int inf=0x3f3f3f3f;
const int Max_n=1100;

int n;
int a[Max_n],sum[Max_n];
int dp[Max_n][Max_n];

int main()
{
    scanf("%d",&n);
    sum[0]=0;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        sum[i]=sum[i-1]+a[i];
    }
    // 从小区间向大区间进行地推
    for(int d=1;d<n;d++){ //先枚举区间长度
        for(int i=1,j;(j=i+d)<=n;i++){ //再枚举区间起点
            dp[i][j]=inf;
            for(int k=i;k<j;k++)
                dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
            dp[i][j]+=sum[j]-sum[i-1];
        }
    }
    printf("%d\n",dp[1][n]);
    return 0;
}

[四边形不等式优化]:

对于一般方程: f ( i , j ) = o p t { f ( i , k ) + f ( k + 1 , j ) } + w ( i , j ) f(i,j)=opt\{f(i,k)+f(k+1,j)\}+w(i,j) f(i,j)=opt{f(i,k)+f(k+1,j)}+w(i,j)
如果 w w w函数满足区间单调性和四边形不等式性质,则 f f f函数也满足四边形不等式性质,具有决策单调性。

  1. 决策单调性:
    定义 s ( i , j ) s(i,j) s(i,j) f ( i , j ) f(i,j) f(i,j)取得最优值时对应的决策点(即下标),则s(i,j-1)≤s(i,j)<s(i+1,j)
  2. 优化方程:
    f(i,j)=opt{f(i,k)+f(k,j)}+w(i,j) s(i,j-1)≤k≤s(i+1,j),复杂度:O(n2)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int inf=0x3f3f3f3f;
const int Max_n=1100;

int n;
int a[Max_n],sum[Max_n];
int s[Max_n][Max_n],dp[Max_n][Max_n];

int main()
{
    scanf("%d",&n);
    sum[0]=0;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        sum[i]=sum[i-1]+a[i];
        s[i][i]=i;
    }
    // 从小区间向大区间进行地推
    for(int d=1;d<n;d++){ //先枚举区间长度
        for(int i=1,j;(j=i+d)<=n;i++){ //再枚举区间起点
            dp[i][j]=inf;
            for(int k=s[i][j-1];k<=s[i+1][j];k++)
                if(dp[i][k]+dp[k+1][j]<dp[i][j]){
                    dp[i][j]=dp[i][k]+dp[k+1][j];
                    s[i][j]=k;
                }
            dp[i][j]+=sum[j]-sum[i-1];
        }
    }
    printf("%d\n",dp[1][n]);
    return 0;
}

10、数位dp问题

思路: dp思想,枚举到当前位置pos,状态为state(这个就是根据题目来的,可能很多,毕竟dp千变万化)的数量(既然是计数,dp值显然是保存满足条件数的个数)

typedef long long ll;  
int a[20];  
ll dp[20][state];//不同题目状态不同  
ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零  
{  
    //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了  
    if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */  
    //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)  
    if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];  
    /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/  
    int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了  
    ll ans=0;  
    //开始计数  
    for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了  
    {  
        if() ...  
        else if()...  
        ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的  
        /*这里还算比较灵活,不过做几个题就觉得这里也是套路了 
        大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论 
        去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目 
        要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类, 
        前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/  
    }  
    //计算完,记录状态  
    if(!limit && !lead) dp[pos][state]=ans;  
    /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/  
    return ans;  
}  
ll solve(ll x)  
{  
    int pos=0;  
    while(x)//把数位都分解出来  
    {  
        a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行  
        x/=10;  
    }  
    return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛  
}  
int main()  
{  
    ll le,ri;  
    while(~scanf("%lld%lld",&le,&ri))  
    {  
        //初始化dp数组为-1,这里还有更加优美的优化,后面讲  
        printf("%lld\n",solve(ri)-solve(le-1));  
    }  
}  

















































66666666666666666666666666

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值