动态规划

递推求解

含义

  • 动态规划的最基本情况。动态规划常用于求解最优解问题,将待求解问题分成若干子问题,先求解子问题,再通过子问题的答案得到原问题的答案。
  • 动态规划和分治法类似,但不同,动态规划的子问题往往有联系,动态规划会将子问题的答案保存到dp数组中,需要时直接获取,减少重复计算,提高效率

模板和所需元素

  • 由于过于简单,不做过多记录,往往需要以下元素:
    • dp数组
    • dp[0],dp[1]等需要手动输入
    • 通过循环求dp[n]

例题

最大连续子序列和

含义

  • 给定一个序列{A1,A2,A3...An},找出一个连续子序列{Ai...Aj}使得这个子序列的和最大,输出序列和
  • 正常依次计算比较每个子序列的和需要O(n3)
  • 记录前缀和来预处理加速的话是O(n2)
  • dp是O(n),有如下几步:
    • 先开辟数组dp[],dp[i]表示以Ai结尾的连续序列的最大和,最后输出dp[]中的最大值即可
    • 有两种情况可能:
      • dp[i]=Ai,即最大和的连续序列只有一个元素
      • dp[i]=Aj+...+Ai-1+Ai,即最大和有多个元素:从i到j,也就是dp[i]=dp[i-1]+Ai
    • 得到状态转移方程:dp[i]=max(Ai,dp[i-1]+Ai) 且有dp[0]=A0

所需元素

  • A数组
  • dp数组
  • 转移方程:dp[i]=max(Ai,dp[i-1]+Ai),且有dp[0]=A0
  • 输出:dp数组的最大值

模板

int arr[1000000];
int dp[1000000];

int main(){
    int N;
    while(scanf("%d",&N)!=EOF){
        for(int i=0;i<N;i++){
            cin>>arr[i];
        }
        dp[0]=arr[0];
        int ans=-1000000;
        for(int i=1;i<N;i++){
            dp[i]=max(arr[i],dp[i-1]+arr[i]);
            ans=max(ans,dp[i]);
        }
        cout<<ans<<endl;
    }
    return 0;
}

例题

  • 最大序列和:https://www.nowcoder.com/practice/df219d60a7af4171a981ef56bd597f7b

  • 最大子矩阵:https://www.nowcoder.com/practice/a5a0b05f0505406ca837a3a76a5419b3

    该题为典型的变形题,附模板:

    思路:设最大子矩阵所在行为i到j,会有两种情况:

    • i=j,问题变为 把第i行当作A,求一维的
    • i!=j,将i行到j行的元素叠加起来当作A,求一维的
    #include<iostream>
    #include<cstdio>
    using namespace std;
    
    int matrix[110][110];
    int total[110][110];
    int arr[110];
    int dp[110];
    
    int MaxSubSequence(int n){
        int maxi=0;
        dp[0]=arr[0];
        for(int i=1;i<n;i++){
            dp[i]=max(dp[i-1]+arr[i],arr[i]);
            maxi = max(maxi,dp[i]);
        }
        return maxi;
    }
    
    int MaxSubMatrix(int n){
        int maxi=0;
        for(int i=0;i<n;i++){
            for(int j=i;j<n;j++){
                for(int k=0;k<n;k++){
                    if(i==0){
                        arr[k]=total[j][k];
                    }
                    else{
                        arr[k]=total[j][k]-total[i-1][k];
                    }
                }
                int current=MaxSubSequence(n);
                maxi = max(maxi,current);
            }
        }
        return maxi;
    }
    
    int main(){
        int n;
        while(scanf("%d",&n)!=EOF){
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    cin>>matrix[i][j];
                }
            }
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    if(i==0){
                        total[i][j]=matrix[i][j];
                    }
                    else{
                        total[i][j]=total[i-1][j]+matrix[i][j];
                    }
                }
            }
            cout<<MaxSubMatrix(n);
        }
        return 0;
    }
    
  • 最大连续子序列:https://www.nowcoder.com/practice/afe7c043f0644f60af98a0fba61af8e7

最长递增子序列

含义

  • 给定一个序列{A1,A2...An},取出若干元素(可以不连续)组成子序列,要求子序列递增。求最长的子序列的长度
  • 如果正常遍历,每个元素可以拿或者不拿,时间复杂都O(2^n)
  • dp可以O(n2)
    • 开辟数组dp[],dp[i]用来存放以Ai结尾的最长递增子序列长度,最终答案是dp[]的最大值
      • dp[i]=1,如果Ai前的元素都比Ai大
      • 如果i之前存在j,使得Aj小于Ai,那么dp[i]=dp[j]+1
    • 从i开始遍历,每次都遍历比i小的那些元素
    • 得到转移方程:dp[i]=max(1,dp[j+1] | j<i && Aj<Ai),初始化dp[i]=1

所需元素

  • A数组
  • dp数组
  • 转移方程:dp[i]=max(1,dp[j+1] | j<i && Aj<Ai),初始化dp[i]=1(或者为Ai,为了求和)
  • 输出:dp数组的最大值

模板

#include<iostream>
#include<cstdio>
using namespace std;

int arr[1001];
int dp[1001];

int main(){
    int n;
    while(scanf("%d",&n)!=EOF){
        for(int i=0;i<n;i++){
            cin>>arr[i];
        }
        int ans=0;
        for(int i=0;i<n;i++){
            dp[i]=arr[i];			//本题要求的是 最长递增子序列的和是多少,否则dp[i]=1
            for(int j=0;j<i;j++){
                if(arr[i]>arr[j]){
                    dp[i]=max(dp[i],arr[i]+dp[j]);
                }
            }
            ans = max(ans,dp[i]);
        }
        cout<<ans<<endl;
    }
}

例题

最长公共子序列

含义

  • 给出字符串S1,S2,分别求其子串(按照先后顺序,但不用连续),找出最长的公共子串

  • 正常遍历,时间复杂度为O(2^m+n)

  • dp时间复杂度为O(nm)

    • 创建二维数组dp[] [],dp[i] [j]为以S1i和S2j结尾的最长公共子序列长度,dp[n] [m]为答案

      • 如果S1i=S2j,那么最起码该位置是存在公共子序列的,所以dp[i] [j]=dp[i-1] [j-1]+1
      • 如果不等,此时最长公共子串为max(S1的前i-1个字符和S2的前j个字符的最长公共字串长度,S1的前i个字符和S2的前j-1个字符的最长公共字串长度),所以dp[i] [j]=max(dp[i-1] [j],dp[i] [j-1])
    • 得到状态转移方程 i=j时,dp[i] [j]=dp[i-1] [j-1]+1

      i!=j时,dp[i] [j]=max(dp[i-1] [j],dp[i] [j-1])

      初始状态:dp[i] [0]=dp[0] [j]=0

所需元素

  • S1,S2

  • 二维dp数组

  • 转移方程:

    i=j时,dp[i] [j]=dp[i-1] [j-1]+1

    i!=j时,dp[i] [j]=max(dp[i-1] [j],dp[i] [j-1])

    初始状态:dp[i] [0]=dp[0] [j]=0

  • 输出:dp数组[n] [m]

模板

#include<iostream>
#include<cstdio>
using namespace std;

string s1;
string s2;
int dp[101][101];

int main(){
    while(cin>>s1>>s2){
        int n=s1.length();
        int m=s2.length();
        for(int i=0;i<=n;i++){
            for(int j=0;j<=m;j++){
                if(i==0||j==0){
                    dp[i][j]=0;
                    continue;
                }
                if(s1[i-1]==s2[j-1]){			//这里一定是i-1和j-1,因为从0开始,最后输出nm
                    dp[i][j]=dp[i-1][j-1]+1;
                }
                else{
                    dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        cout<<dp[n][m]<<endl;
    }
    return 0;
}

例题

背包问题

0-1背包

含义

  • 有n个物品,每个物品wi沉,vi贵,现有容量m的背包,问怎么拿能使得价值最大
  • 普通排列组合,一个物品有拿或者不拿两个选择,时间复杂度O(2^n)
  • dp的时间复杂度为O(nm):
    • 开辟二维数组dp[] []:dp[i] [j]表示前i个物品装进容量为j的背包能获得的最大价值。dp[n] [m]为答案
    • 对于容量为j的背包,如果不放入第i个物品,那么dp[i] [j]=dp[i-1] [j]
    • 如果放入第i个物品,那么dp[i] [j]=dp[i-1] [j-w[i]] + v[i]
    • 得到状态转移方程:
      • dp[i] [j]=max(dp[i-1] [j],dp[i-1] [j-w[i]]+v[i]),但要求j-w[i]要大于等于0
      • 初始状态:dp[i] [0]=dp[0] [j]=0
    • 优化状态转移方程:由于i一直是+1的,所以用一维数组代替:
      • dp[j]=max(dp[j],dp[j-w[i]]+v[i])
      • 但要求,为了保证状态正确转移,必须保证在每次更新中确定状态dp[j]时,dp[j-w[i]]未被本次更新修改,所以需要每次更新,都倒叙遍历所有j的值
      • 这里倒叙查j是考虑覆盖问题,参见https://blog.csdn.net/aidway/article/details/50726472

所需元素

  • w数组,v数组

  • 一维dp数组

  • 转移方程:

    dp[j]=max(dp[j],dp[j-w[i]]+v[i]) 要求倒叙查j,并且j-w[i]要大于等于0

    初始状态:dp[j]=0

  • 输出:dp[m]

模板

#include<iostream>
#include<cstdio>
using namespace std;

int v[1001];
int w[1001];
int dp[1001];

int main(){
    int n,m;
    cin>>m>>n;
    for(int i=0;i<n;i++){
        cin>>w[i]>>v[i];
    }
    for(int i=0;i<=m;i++){
        dp[i]=0;
    }
    for(int i=0;i<n;i++){
        for(int j=m;j>=w[i];j--){			//倒叙!
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

例题

完全背包

含义

  • 与0-1背包不同,完全背包可以去无限取某一物品
  • 解决方法同0-1背包,只不过转移方程变为:
    • dp[i] [j]=max(dp[i-1] [j],dp[i] [j-w[i]]+v[i]),但要求j-w[i]要大于等于0
    • 初始状态:dp[i] [0]=dp[0] [j]=0
  • 同样的,优化后的方程为:dp[j]=max(dp[j],dp[j-w[i]]+v[i]),只不过j从逆序遍历变成正序遍历

所需元素

  • w数组,v数组

  • 一维dp数组

  • 转移方程:

    dp[j]=max(dp[j],dp[j-w[i]]+v[i]) 要求顺序查j,并且j-w[i]要大于等于0

    初始状态:dp[j]=0

  • 输出:dp[m]

模板

const int INF=0x3f3f3f3f;
int v[10010];
int w[10010];
int dp[1001000];

int main(){
	int k;
	cin>>k;
	while(k--){
		memset(v,0,sizeof(v));
		memset(w,0,sizeof(w));
		int a,b,ans;
		cin>>a>>b;
		ans=b-a;
		int n;
		cin>>n;
		for(int i=0;i<n;i++){
			cin>>v[i]>>w[i];
		}
		memset(dp,INF,sizeof(dp));	//初始化数组无穷大
        
        
		dp[0]=0;	//但是第一个一定要为0,如果不为零,那么数组中所有的数都会是无穷大的。
		for(int i=0;i<n;i++){
			for(int j=w[i];j<=ans;j++){		//顺序
				dp[j]=min(dp[j],dp[j-w[i]]+v[i]);
			}
		}
        
        
		if(dp[ans]!=INF){
			printf("The minimum amount of money in the piggy-bank is %d.\n",dp[ans]);
		}else{
			cout<<"This is impossible."<<endl;
		}
	}
}

例题

多重背包

含义

  • 有n个物品,每个物品wi沉,vi贵,且有ki个,现有容量m的背包,问怎么拿能使得价值最大
  • 将ki个物品视为k个质量、价值都相同的物品求0-1背包,时间复杂度是O(mΣki)
  • 解决:将数量为k的物品拆分为若干组,将每组视为一个新物品,其w和v为组内物品总和。现要求每组包含的物品个数为20,21....2c-1**,k-2c+1,其中c是使得k-2^c+1>=0的最大整数,然后再0-1背包,此时的时间复杂度为O(mΣlog(ki))**

所需元素

  • 同0-1背包

模板

#include<iostream>
#include<cstdio>
using namespace std;

int v[10000];
int w[10000];
int k[10000];
int dp[10000];
int value[10000];		//分解后的v
int weight[10000];		//分解后的w

int main(){
	int C;
	cin>>C;
	for(int i=0;i<C;i++){
		int n,m;
		cin>>m>>n;
		int number=0;		//记录分解后有几个
		for(int j=0;j<n;j++){
			cin>>w[j]>>v[j]>>k[j];
			for(int p=1;p<=k[j];p<<=1){		//分解物品,p是2的c次方增长
				value[number]=p*v[j];
				weight[number]=p*w[j];
				number++;
				k[j]-=p;
			}
			if(k[j]>0){					//k-2^c+1个
				value[number]=k[j]*v[j];
				weight[number]=k[j]*w[j];
				number++;
			}
		}
		
		for(int j=0;j<=m;j++){				//初始化dp数组
			dp[j]=0;
		}
		for(int j=0;j<number;j++){				//m是容量,number是分解后有几个物品
			for(int k=m;k>=weight[j];k--){
				dp[k]=max(dp[k],dp[k-weight[j]]+value[j]);
			}
		}
		cout<<dp[m]<<endl;
	} 
	return 0;
} 

例题

posted @ 2021-03-31 21:34  LazyXx  阅读(70)  评论(0编辑  收藏  举报