动态规划

动态规划
y总分析法nb!!!
步骤:
1.首先对题目分析,写出一个涵盖所有的状态,就像是写深搜的函数头一样,需要多做题,自然就有感觉了hh
2.状态计算,即状态划分,将这个状态不重不漏的进行划分,
然后分析状态转移方程;最后记得考虑边界情况!!!

动态规划实质上时枚举出每种状态然后找出最优解;
枚举一般用循环或者递归来实现:
1.斐波那契数列
要计算第n项的值就必须计算出n-1和n-2的值,计算n-1,必须先计算出n-2
和n-3的值,一直到1和2为止;

int fun(int n){
    if(n==1) return 1;
    if(n==2) return 2;
    return fun(n-1)+fun(n-2);
}

画出图像之后会发现有大量的重叠子问题,这些重叠子问题可以避免重复计算,使用一个数组把已经计算过的值存储起来,需要时直接返回即可;
当把这些重叠子问题删除掉之后,就会发现这个树形会转化成一个线性问题,从而可以使用递推来求解;
2.01背包
题目不过多解释,方程f(i,j)=max(f(i-1,j),f(i-1,j-v[i])+w[i]),这里选取第i个物品时,就相当于再前i-1个物品里面选,体积不能超过j-v[i];

int fun(int i,int j){
		if(i==0) return 0;
		if(dp[i][j]) return dp[i][j];
		int res=-10;
		res=max(res,fun(i-1,j));
		if(j>=v[i]) res=max(res,fun(i-1,j-v[i])+w[i]);
		dp[i][j]=res;
		return dp[i][j];
	}

转化成递推,有i,j两个参数,所以需要两层嵌套才能枚举;

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int f[N][N],w[N],v[N];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++){
            f[i][j]=f[i-1][j];
            if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    cout<<f[n][m]<<endl;
    return 0;
}

优化:因为每次计算第i层时都只用到了第i-1层,所有i这个维度可以删掉;
删掉之后变成了f[j]=max(f[j],f[j-v[i]]+w[i])
,又因为j是从小到大枚举的,计算f[j]的时候实际上使用的是第i层而不是第i-1层;(最后输出的f[m]肯定是前n层里面的最优解,如果是从小到大枚举的话,f[m]最终只会变成第i层的最优解),如果要使用第i-1层的结果,就需要从大到小枚举;

#include<iostream>
#include<algorithm>

using namespace std;
const int N=1010;
int f[N],w[N],v[N];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    for(int i=1;i<=n;i++)
        for(int j=m;j>=0;j--){
            
            if(j>=v[i]) f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    cout<<f[m]<<endl;
    return 0;
}

在这里插入图片描述

2.完全背包:
完全背包的转移方程:
f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,…)
f[i][j-v]=max( f[i-1][j-v],f[i-1][j-2v]+w,…)
可以发现f[i][j]除去第一项,剩下的就是第二个公式的最大值;
因此转移方程为
f[i][j]=max(f[i-1][j],f[i][j-v]+w);

优化:同理他只用到了上一层,因此可以减去一维,这里减去一维之后不需要从大到小枚举,因为转移方程f[j-v]+w指的就是第i项

#include<iostream>
#include<algorithm>

using namespace std;
const int N=1010;
int f[N][N],w[N],v[N];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++){
            f[i][j]=f[i-1][j];
            if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
        }
    cout<<f[n][m]<<endl;
}
//优化
#include<iostream>
#include<algorithm>

using namespace std;
const int N=1010;
int f[N],w[N],v[N];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    for(int i=1;i<=n;i++)
        for(int j=0;j<=m;j++)
            if(j>=v[i]) f[j]=max(f[j],f[j-v[i]]+w[i]);
            
    cout<<f[m]<<endl;
    
    
    
    return 0;
}

多重背包:
给定每个物品最多的个数si;
状态转移方程,
f[i][j]=max(f[i][j],f[i-1][j-k * v[i]]+kw[i]);
这样就需要三层嵌套循环,假如数据过大肯定会超时;
优化思路:二进制优化;
首先,二进制可以枚举表示一个数,数字s可以表示为,前2^k的和跟s-减去这些和,相当于是把这个数字分成k+1堆,这些数字可以枚举任何一个0到s之间的数字(相当于是选多少个物品),==这样就可以把n
ms的时间复杂度降低至n
m*logs,然后每一堆只能选择一次就可以表示所有数字,相当于是重新分配之后的01背包==;

//数据范围最大为2000,显然不能用朴素想法实现//
#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;
const int N=2010,M=15000;
int f[N],v[M],w[M],s[M];
int main(){
    int n,m;
    cin>>n>>m;
    int cnt=1;//重新分堆的个数
    for(int i=0;i<m;i++){
        int t=1;
        int a,b,s;
        cin>>a>>b>>s;
        while(t<=s){//二进制分堆;
            v[cnt]=a*t;
            w[cnt]=b*t;
            s-=t;
            t*=2;
            cnt++;
        }
        if(s>0) v[cnt]=a*s,w[cnt]=b*s,cnt++;//最后剩余的一定要加进去;
    }
    for(int i=1;i<cnt;i++)
        for(int j=m;j>=0;j--)
            if(j>=v[i]) f[j]=max(f[j],f[j-v[i]]+w[i]);
    cout<<f[m]<<endl;
    
    return 0;
}

数字三角形
(记忆化递归,正序,正序+优化,倒序,倒序+优化)

//记忆化递归;
#include<iostream>
#include<algorithm>
using namespace std;
const int N=550;
int n;
int a[N][N],dp[N][N];
int fun(int x,int y){
    if(x==n+1) return 0;
    if(dp[x][y]) return dp[x][y];
    dp[x][y]=max(fun(x+1,y)+a[x][y],fun(x+1,y+1)+a[x][y]);
    return dp[x][y];
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
        cin>>a[i][j];
    cout<<fun(1,1)<<endl;
    
    return 0;
}
//————————————————————————————————————————————————————————————//
//正序;
#include<iostream>
#include<algorithm>

using namespace std;
const int N=550,INF=1e9;
int a[N][N],f[N][N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>a[i][j];
    for(int i=1;i<=n;i++)
        for(int j=0;j<=i+1;j++)
            f[i][j]=-INF;
    f[1][1]=a[1][1];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            f[i][j]=max(f[i-1][j],f[i-1][j-1])+a[i][j];
    int res=-INF;
    for(int i=1;i<=n;i++) res=max(res,f[n][i]);
    cout<<res<<endl;
    
    
    
    return 0;
}
//正序优化
#include<iostream>
#include<algorithm>

using namespace std;
const int N=550,INF=1e9;
int a[N][N],f[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>a[i][j];
    for(int j=0;j<=n+1;j++)
            f[j]=-INF;
    f[1]=a[1][1];
    for(int i=2;i<=n;i++){
        
        for(int j=i;j>=1;j--)
            f[j]=max(f[j],f[j-1])+a[i][j];
    }
    int res=-INF;
    for(int i=1;i<=n;i++) res=max(res,f[i]);
    cout<<res<<endl;
    
    
    
    return 0;
}
//倒序;
#include<iostream>
#include<algorithm>

using namespace std;
const int N=550;
int a[N][N],f[N][N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>a[i][j];
    for(int i=n;i>=1;i--)
        for(int j=1;j<=i;j++)
            f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
    cout<<f[1][1]<<endl;
    return 0;
}
//倒序+优化;
#include<iostream>
#include<algorithm>

using namespace std;
const int N=550;
int a[N][N],f[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            cin>>a[i][j];
    for(int i=n;i>=1;i--)
        for(int j=1;j<=i;j++)
            f[j]=max(f[j],f[j+1])+a[i][j];
    cout<<f[1]<<endl;
    return 0;
}

数字三角形模型题:
1.最低通行费

/*
根据题意的在2*n-1步走到终点,自己模拟一遍可以发现
这就意味着不能往回走;
同时也就是数字三角形的dp模型;
计算最小值必须注意边界情况;
最小值一遍先将数组赋一个很大的值;
然后处理边界情况即可;
*/
#include<iostream>
#include<cstring>
using namespace std;
const int N=110;
int w[N][N],f[N][N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++)
            cin>>w[i][j];
    }
    memset(f,0x3f,sizeof f);//赋一个较大的值;
    f[0][1]=f[1][0]=0;//因为1,1点只能由1,0,或者0,1这两个点过来,这两个点为0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++)
            f[i][j]=min(f[i-1][j],f[i][j-1])+w[i][j];
    }
    cout<<f[n][n]<<endl;
    return 0;
}

最长上升子序列;
状态分析:f(i),表示前i个数字中的最长的子序列,f[i]=max(f[i],f[j]+1);

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int f[N],a[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++){
        f[i]=1;
        for(int j=1;j<i;j++)
            if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
    }
    int res=0;
    for(int i=1;i<=n;i++) res=max(res,f[i]);
    cout<<res<<endl;
    return 0;
}

区间dp
区间dp套路:

  for(int len=2;len<=n;len++){//枚举区间
        for(int l=1;i+len-1<=n;i++)//起点
            int r=i+len-1;//终点;
            for(int k=l;k<r;k++){//划分边界;
                dp[l][r]=max/min(dp[l][r],dp[l][k]+dp[k+1][r]+....);
            }
    }

题目:link.
状态表示f[i][j] 表示为区间i,j之间的所有选法,在其中取得最小值;
状态转移方程: f[i][j]=min(f[i][k]+f[k+1][j]+s[j]-s[i-1])
其中s表示为前缀和,k为最后一次的划分边界;

/*
区间dp:
状态表示:f[i][j]表示从i到j的区间;
状态计算:可以划分为j-i-1个区间,枚举这些区间找到最小值;
区间dp一般套路:
1.枚举区间长度,len;
2.枚举左端点,同时右端点不能超过区范围;
3.通过左端点找到右端点,即左端点+区间长度-1,
4.找到一个合适的分界点,将分界点两边求和即可
*/
#include<iostream>

using namespace std;
const int N=350;
int f[N][N],s[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];
    for(int len=2;len<=n;len++){//区间为长度为1,没有意思所以从2开始枚举
        for(int l=1;l+len-1<=n;l++){//左端点;
            int r=l+len-1;//右端点
            f[l][r]=1e8;
            for(int j=l;j<=r;j++){//
                f[l][r]=min(f[l][r],f[l][j]+f[j+1][r]+s[r]-s[l-1]);
            }
        }
    }
    cout<<f[1][n]<<endl;
    return 0;
}

最长公共子序列;
链接:最长公共子序列

/*
状态表示:f(i,j),表示a的前i个字母和b的前j个字母组成的公共子序列
状态划分:1表示选,0表示不选,
总共由,10,00,01,11,四种状态;
其中10表示a的前i个字母,b的前j-1个字母,01同理;这两个实际上表示的不是
选与不选i和j,但是包含了这种情况,可以使用这两个状态来代替;
*/
#include<iostream>
#include<cstring>

using namespace std;
const int N=1010;
char a[N],b[N];
int f[N][N];
int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    scanf("%s%s",a+1,b+1);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            f[i][j]=max(f[i-1][j],f[i][j-1]);
            if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

最短编辑次数

/*
**状态分析: dp(i,j)表示第1-i和1-j的字符串的所需要的修改次数;
**状态计算: {
    1.删除操作,当要删除第i个字符的时候,说明第i-1和第j个字符相同的,dp[i-1][j]+1;
    2.插入操作,在第i个字符的位置插入一个字符时说明,第i个字符和第j-1个字符是相同的,dp[i][j-1]+1;
    3.修改操作,当第i个和第j个不同时,说明第i-1和j-1时相同的,即dp[i-1][j-1]+1;
    如果相同的话,则不需要进行任何操作,即dp[i-1][j-1];
}

*/
#include<iostream>
#include<cstring>

using namespace std;
const int N=1010;
char a[N],b[N];
int dp[N][N];
int main(){
    int n,m;
    scanf("%d%s%d%s",&n,a+1,&m,b+1);
    //预处理
    for(int i=1;i<=m;i++) dp[0][i]=i;//第0个和第i个需要匹配时,需要增加i个字符
    for(int i=1;i<=n;i++) dp[i][0]=i;//第i个和第0个匹配时,需要删除i个字符;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            dp[i][j]=min(dp[i-1][j],dp[i][j-1])+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);
        }
    }
    cout<<dp[n][m]<<endl;
    return 0;
}

计数类dp
整数划分

/*
完全背包做法;
题目:一个数字用1~n中的数字表示;
并且可以有重复,很显然是完全背包的模型;
这里存储的是方案个数,所以转移方程为
f[i][j]=(f[i-1][j]+f[i][j-i])
转化成一维就是
f[j]=f[j]+f[j-i];
*/
#include<iostream>

using namespace std;
const int N=1010;
int f[N];
const int mod=1e9+7;
int main(){
    int n;
    cin>>n;
    f[0]=1;//边界情况;这里的边界情况不是表示0只有一种表示方法,
    //而是一个数字n用n表示只有一种方法,因为f[n-n]=f[0];
    for(int i=1;i<=n;i++){
        for(int j=i;j<=n;j++){
            f[j]=(f[j]+f[j-i])%mod;
        }
    }
    cout<<f[n]<<endl;
    return 0;
}

方格选数

/*
不能dp两次的原因,因为第一次
选择的是最优解,第二次选择的只是在
第一次的基础上选择的最优解;
并不是两次加起来的最优解;
所以需要两次同时走;
f[i1][j1][i2][j2];
这个状态是同时走的,所以
i1+j1=i2+j2;
所以可以优化一维,变成f[k][i1][i2];k表示i+j;
一共有四种情况,同时判断两个是否重合即可
*/
#include<iostream>
#include<algorithm>

using namespace std;
const int N=15;
int g[N][N],f[N+N][N][N];

int main(){
    int n;
    cin>>n;
    int x,y,w;
    while(cin>>x>>y>>w , x or y or w){
        g[x][y]=w;
    }
    for(int k=2;k<=n+n;k++){
        for(int i1=1;i1<=n;i1++){
            for(int i2=1;i2<=n;i2++){
                int j1=k-i1,j2=k-i2;
                if(j1>=1 and j1<=n and j2>=1 and j2<=n){
                    int t=g[i1][j1];
                    if(i1!=i2) t+=g[i2][j2];
                    int &x=f[k][i1][i2];
                    x=max(f[k-1][i1-1][i2-1]+t,x);
                    x=max(f[k-1][i1][i2]+t,x);
                    x=max(f[k-1][i1-1][i2]+t,x);
                    x=max(f[k-1][i1][i2-1]+t,x);
                }
            }
        }
    }
    cout<<f[n+n][n][n]<<endl;
    return 0;
}

最低通行费

/*
根据题意的在2*n-1步走到终点,自己模拟一遍可以发现
这就意味着不能往回走;
同时也就是数字三角形的dp模型;
计算最小值必须注意边界情况;
最小值一遍先将数组赋一个很大的值;
然后处理边界情况即可;
*/
#include<iostream>
#include<cstring>
using namespace std;
const int N=110;
int w[N][N],f[N][N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++)
            cin>>w[i][j];
    }
    memset(f,0x3f,sizeof f);//赋一个较大的值;
    f[0][1]=f[1][0]=0;//因为1,1点只能由1,0,或者0,1这两个点过来,这两个点为0;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++)
            f[i][j]=min(f[i-1][j],f[i][j-1])+w[i][j];
    }
    cout<<f[n][n]<<endl;
    return 0;
}

求方案数(数字三角形模型);
过河卒

/*
数字三角形模型
转移方程f[i][j]=f[i-1][j]+f[i][j-1];
*/
#include<iostream>

#include<algorithm>

using namespace std;
const int N=25;
typedef long long ll;
int dx[9]={0,2,2,-2,-2,1,1,-1,-1},dy[9]={0,1,-1,1,-1,2,-2,2,-2};
ll g[N][N],f[N][N];
int main(){
    int n,m;
    int x,y;
    cin>>n>>m>>x>>y;
    n++,m++,x++,y++;
    f[0][1]=1;//1,1点只有一种方案;
    for(int i=0;i<9;i++){
        int a=x+dx[i],b=y+dy[i];
        g[a][b]=-1;//马可以到达的点做上标记;
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(g[i][j]==-1) continue;
            f[i][j]=f[i-1][j]+f[i][j-1];
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

最大子段和

/*
因为是连续的子序列
所以f[i]一定是由f[i-1]或者本身转移过来的;
f[i]=amx(f[i-1]+a[i],a[i])
*/
#include<iostream>
#include<algorithm>

using namespace std;

const int N=2e5+10;
int f[N],a[N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++){
        f[i]=max(f[i-1]+a[i],a[i]);
    }
    int res=-10000000;
    for(int i=1;i<=n;i++) res=max(res,f[i]);
        cout<<res<<endl;



    return 0;
}

二维拓展
最大加权矩形
注意!这个题目不能直接按照二位前缀和枚举,这样会将某些情况
覆盖掉

/*
思路类似与最大子段和,
首先将矩阵压缩成一个一维;
然后按照最大子段和计算;
分别枚举边界,i,j,k;
其中i表示上边界,j表示下边界,
k表示右边界,便压缩成了一个
一维数组进行计算;
*/
#include<iostream>
#include<algorithm>

using namespace std;
const int N=150;
int g[N][N];
int main(){
    int n;
    cin>>n;
    int x;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>x;
            g[i][j]=g[i-1][j]+x;  
        }
    }
    int res=-100000;
    for(int i=1;i<=n;i++){
        for(int j=i;j<=n;j++){
            int f=0;
            for(int k=1;k<=n;k++){
                
                f=max(0,f)+g[j][k]-g[i-1][k];
                res=max(res,f);
            }
        }
    }
    cout<<res<<endl;
    return 0;
}

打怪

//dp做法;
#include<iostream>
#include<algorithm>

using namespace std;

const int N=5010;
int x[N],y[N],f[N];
int main(){
    int n,w;
    cin>>n>>w;
    int a,b;
    cin>>a>>b;
    a+=b;
    for(int i=1;i<=n;i++) cin>>x[i]>>y[i];
    for(int i=1;i<=n;i++){
        for(int j=w;j>=y[i];j--){
            if(a>=x[i]) f[j]=max(f[j],f[j-y[i]]+1);
        }
    }
    cout<<f[w]<<endl;
    
    
    
    
    return 0;
}
//贪心做法
#include<iostream>

#include<algorithm>

using namespace std;
#define x first
#define y second
typedef pair<int,int> pii;
const int N=5010;
pii f[N];
bool cmp(pii a,pii b) {
    return a.y<b.y;
}
int main(){
    int n,w;
    cin>>n>>w;
    int a,b;
    cin>>a>>b;
    a+=b;
    for(int i=0;i<=n;i++) cin>>f[i].x>>f[i].y;
    sort(f,f+n,cmp);
    int cnt=0;
    for(int i=0;i<n;i++){
        if(a<f[i].x) continue;
        if(w-f[i].y<0) break;
         w-=f[i].y,cnt++;
       
    }
    cout<<cnt<<endl;
    return 0;
}

金币馅饼

#include<iostream>
#include<algorithm>

using namespace std;
const int N=110;
int f[N][N],g[N][N];
void solve(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
               cin>>g[i][j];
    f[1][1]=g[1][1];
    for(int j=1;j<=m;j++){
        for(int i=1;i<=n;i++){
            if(f[i-1][j-1] or f[i][j-1] or f[i+1][j-1]){
                f[i][j]=max(f[i-1][j-1] ,max(f[i][j-1],f[i+1][j-1]))+g[i][j];
            }
        }
    }
    cout<<f[n][m]<<endl;
    
    
    
}
int main(){
    solve();
    
    
    
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值