《算法笔记》编程笔记——第十一章 动态规划

《算法笔记》编程笔记——第十一章 动态规划

  • 动态规划DP,是用来解决一类最优化问题的算法思想。动态规划会将每个求解过的子问题的解记录下来,这样当下一次遇到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算。动态规划一般可以用递推或者递归的方式来写。递归写法又叫做记忆型搜索。

  • 一个问题必须拥有重叠子问题最优子结构,才能使用动态规划去解决。

  • 使用递推写法的计算方式是自底向上,即从边界开始,不断向上解决问题,知道解决了目标问题;而使用递归写法的计算方式是自顶向下,即从目标问题开始,将他分解成子问题的组合,直到分解至边界为止。

  • 典型简单例题:

    • 【斐波那契数列计算】代码如下:

      int dp[n];
      memset(dp, -1, sizeof(dp));//dp数组初始化为-1.
      int F(int n){
        if(n == 0 || n == 1)return 1;//边界条件
          if(dp[n] != -1)return dp[n];//表示已经计算过,直接返回计算结果
          else{
              dp[n] = F(n - 1) + F(n - 2);//式子
              return dp[n];
        }
      }
      
    • 【数塔问题】

      • 题目描述:将一些数字排成数塔的形状,其中第一层有1个数字,第二层有2个……第n层有n个数字。现在要从第一层走到第n层,每次只能走向下一层连接的两个数字中的一个,问最后将路径上所有数字相加后得到的和最大是多少。

      • 题目思路:

        //另dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径中能得到的最大和.由此可以得到以下状态转移方程:
        dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j];
        //数塔最后一层的dp值总是等于元素本身,于是有dp[n][j] = f[n][j](1<= j <= n);这 称为边界。
        
      • 代码实现:

        #include<bits/stdc++.h>
        using namespace std; 
        const int maxn = 1000;
        int f[maxn][maxn], dp[maxn][maxn];
        int main(){
            int n;
            cin >> n;
            for(int i = 1; i <= n; i++){
                for(int j = 1; j <= i; j++){
                    cin >> f[i][j];
                }
            }
            //边界
            for(int j = 1; j <= n; j++){
                dp[n][j] = f[n][j];
            }
            //从第n-1层不断往上计算出dp[i][j]
            for(int i = n - 1; i >= 1; i--){
                for(int j = 1; j <= i; j++){
                    //状态转移方程
                    dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j];
                }
            }
            //输出结果,dp[1][1]为所需要答案
            printf("%d", dp[1][1]);
            return 0;
        }
        
  • 题目:【地宫取宝】

    • X 国王有一个地宫宝库。是 n x m 个格子的矩阵。每个格子放一件宝贝。每个宝贝贴着价值标签。
      地宫的入口在左上角,出口在右下角。
      小明被带到地宫的入口,国王要求他只能向右或向下行走。
      走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它(当然,也可以不拿)。
      当小明走到出口时,如果他手中的宝贝恰好是k件,则这些宝贝就可以送给小明。
      请你帮小明算一算,在给定的局面下,他有多少种不同的行动方案能获得这k件宝贝。

      【数据格式】
      输入一行3个整数,用空格分开:n m k (1<=n,m<=50, 1<=k<=12)
      接下来有 n 行数据,每行有 m 个整数 Ci (0<=Ci<=12)代表这个格子上的宝物的价值
      要求输出一个整数,表示正好取k个宝贝的行动方案数。该数字可能很大,输出它对 1000000007 取模的结果。

      例如,输入:
      2 2 2
      1 2
      2 1
      程序应该输出:
      2

      再例如,输入:
      2 3 2
      1 2 3
      2 1 5
      程序应该输出:
      14

      资源约定:
      峰值内存消耗 < 256M
      CPU消耗 < 1000ms

    • 代码如下:

      #include<cstdio>
      #include<cstring>
      #define N 1000000007
      int n, m, k;
      int map[55][55];
      int dp[55][55][15][15]; //dp[x][y][num][val]的形式,表示
      						//x,y为坐标,num表示目前选了多少件物品,val表示物品的最大价值
      						//那么dp[1][2][3][4] = 2;表示的意思就是在坐标(1, 2)的位置上,有3件物品
      						//物品的最大价值是4,且当前这个点到达终点有2条路径。 
      int dfs(int x, int y, int num, int max){
      	if(dp[x][y][num][max+1] != -1){//表示此条路已经进入了记忆系统,直接调用记忆过的即可。 
      		return dp[x][y][num][max + 1];//同时注意,因为物品的value值可能为0,所以max初始设置为-1,那么就不能直接当做数组下标使用 
      	}
      	//从终点开始计数
      	if(x == n && y == m){
      		if(num == k || (num == k - 1 && map[x][y] > max)){
      			return dp[x][y][num][max + 1] = 1;   //满足条件,当前点到目标有一种方案。是终点也算一种的意思????? 
      		}
      		return dp[x][y][num][max + 1] = 0;
      	} 
      	long long ans = 0;
      	//向下走,也就是x增加
      	if(x < n){
      		if(map[x][y] > max){
      			ans += dfs(x + 1, y, num + 1, map[x][y]);
      		}
      		ans += dfs(x + 1, y, num, max);
      	} 
      	//如果向右走 
      	if(y < m){
      		if(map[x][y] > max){
      			ans += dfs(x, y + 1, num + 1, map[x][y]);
      		} 
      		ans += dfs(x, y + 1, num, max);
      	}
      	return dp[x][y][num][max + 1] = ans % N;   //注意要更新dp数组 
      }
      int main(){
      	scanf("%d%d%d", &n, &m, &k);
      	for(int i = 1; i <= n; i++){
      		for(int j = 1; j <= m; j++){
      			scanf("%d", &map[i][j]);
      		}
      	}
      	//初始化dp数组
      	memset(dp, -1, sizeof(dp)); 
      	//调用dfs函数
      	dfs(1, 1, 0, -1);
      	printf("%d", dp[1][1][0][0]);//也就是从终点开始算起,初始化路径为1;那么起点就是所有的路径条数。 
      	return 0;
      } 
      
  • 经典动态规划模型

    • 最大连续子序列和

      • 问题描述:给定一个数字序列A1, A2……An,求i,j(1 <= i <= j <= n),使得Ai + …… + Aj 最大,输出这个最大和

        • 分析:如果使用暴力法来做,时间复杂度为O(n^2);如果使用动态规划来做,时间复杂度为O(n)。
      • 思路:

        ①另状态dp[i]表示以A[i]作为末尾的连续序列的最大和;那么求解最大和转换为求解dp数组的最大值。

        ②对状态转移情况进行考虑:1)这个最大和的连续序列只有一个元素,即A[i]。2)这个最大和的连续序列有多个元素,即从A[p]开始,(p< i)。

        于是可以得到状态转移方程

        dp[i] = max(A[i], dp[i-1] + A[i]);
        
      • 代码如下:

        #include<cstdio>
        #include<algorithm>
        using namespace std;
        const int maxn = 10010; //注意:如果要用一个变量来表示一个数据,那么需要用const 
        int a[maxn], dp[maxn];
        int main(){
        	int n;
        	scanf("%d", &n);
        	for(int i = 0; i < n; i++){
        		scanf("%d", &a[i]);
        	}
        	//以下开始动态规划
        	dp[0] = a[0];//动态规划的边界
        	for(int i = 1; i < n; i++){
        		dp[i] = max(a[i], dp[i - 1] + a[i]);
        	} 
        	//寻找dp数组的最大值,也就是最大连续子序列和
        	int k = 0;  //此处用下标来寻找,因为数据本身也可能是负值,那么k的初始值就不好设置了。 
        	for(int i = 1; i < n; i++){
        		if(dp[i] > dp[k]){
        			k = i;
        	}
        	} 
        printf("%d", dp[k]);
        	return 0;
        } 
        
    • 最长不下降子序列(LIS, Longest Increasing Sequence)

      • 问题描述:在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降(非递减)的。

      • 思路:

        ①另dp[i]表示以a[i]结尾的最长不下降子序列长度。如果存在a[i]之前的元素a[j] (j < i),使得a[j] <= a[i]且dp[j] + 1 > dp[i],那么就形成一条更长的不下降子序列,另dp[i] = dp[j] + 1;

        ②如果不存在这样的元素,那么dp[i] = 1;

      • 状态转移方程:

        dp[i] = max{1, dp[j] + 1}(j = 1, 2,…… && a[j] <= a[i])
        //边界:
        dp[i] = 1;//表示每个数字的长度都是1。
        
      • 模板代码(需要注意的是if中的条件有两个,一个自然是序列不降的意思,另一个是算上之前的序列之后,序列的长度必须是增加的,否则何必做无用功呢?)

        #include<cstdio>
        int a[10010], dp[10010];
        int main(){
        	int n;
        	scanf("%d", &n);
        	for(int i = 0; i < n; i++){
        		scanf("%d", &a[i]);
        	}
        	for(int i = 0; i < n; i++){
        		dp[i] = 1; //每个数字的长度为1 
        		for(int j = 0; j < i; j++){
        			//当前一个数小于等于后一个数,并且算上前一个数之后,得保证前一个数的dp+1,得大于后一个数;
        			//如果小于等于,那么即使加上了前一个数,后一个数的子序列长度不会改变甚至会变小。 
        			if(a[i] >= a[j] && (dp[j] + 1 > dp[i])){
        				dp[i] = dp[j] + 1;
        			}
        		}
        	}
        	//寻找最大dp值,即最长不下降序列长度 
        	int maxlen = -1;
        for(int i = 0; i < n; i++){
        		if(maxlen < dp[i]){
        			maxlen = dp[i];
        		}
        	} 
        	printf("%d\n", maxlen);
        	return 0;
        } 
        
    • 最长公共子序列LCS

      • 问题描述:给定两个字符串(或数字序列)A和B,求一个字符串,使得这个字符串是A和B的最长公共部分。(子序列可以不连续)

      • 思路:

        ①令dp[i] [j]表示字符串A的i号位和字符串B的j号位之前的LCS长度(下标从1开始)。

        ②如果A[i] == B[j],则字符串A与字符串B的LCS增加了1位,即有 dp[i] [j] = dp[i-1] [j-1] + 1;

        ③如果A[i] != B[j],则字符串A的i号位与字符串B的j号位之前的LCS无法延长,于是dp[i] [j] = max(dp[i-1] [j], dp[i] [j-1])。

      • 状态转移方程:

        //dp[i][j]表示a字符串中第i个字母和b字符串中第j个字母
        if(a[i] == b[j]){
            //如果有字母相同,那么就dp就是前一个dp + 1
            dp[i][j] = dp[i - 1][j - 1] + 1;
        }else{
            //如果不同,那么dp就是两个之中的最大值
            dp[i][j] = max{dp[i - 1][j], dp[i][j - 1]};
        }
        //边界:
        dp[i][0] = dp[0][j] = 0;
        
        • 模板代码如下:
      //基本思路:输入字符串(开始位置为1)--> 获取字符串的长度(注意长度加1)-->  设置dp边界--> 状态转移方程更新 -->  输出答案
      #include<cstdio>
      #include<algorithm>
      #include<cstring>
      using namespace std;
      char a[110], b[110];
      int dp[110][110];
      int main(){
      	//表示从a[1]的位置开始填入字符串的第一个字母 
      	gets(a + 1);
      	gets(b + 1);
      	//因为从第一个位置开始填入了字符,所以长度也应该 + 1 
      	int lenA = strlen(a + 1);
      	int lenB = strlen(b + 1);
      	//dp的边界
      	for(int i = 0; i <= lenA; i++){
      		dp[i][0] = 0;
      	} 
      	for(int j = 0; j <= lenB; j++){
      		dp[0][j] = 0;
      	}
      	//状态转移方程
      	for(int i = 1; i <= lenA; i++){
      		for(int j = 1; j <= lenB; j++){
      			if(a[i] == b[j]){
      				dp[i][j] = dp[i - 1][j - 1] + 1;
      			}else{
      				dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
      			}
      		}
      	} 
      	//输出
      	printf("%d", dp[lenA][lenB]); 
      	return 0;
      }
      
      • 最长公共子序列变形

      • 问题描述:ABBC与AABC的最长公共子序列为AABBC。即允许公共部分产生重复元素。

      • 状态转移方程:

        if(a[i] == b[j]){//相同时区别于不允许重复的状态转移表达式
            dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + 1;
        }else{
           	dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
        }
        
    • 最长回文子串

      • 问题描述:给出一个字符串s,求s的最大回文子串的长度

      • 错误解法:此方法不可以用将s字符串倒过来变成字符串t,然后寻找s和t的最大公共子序列。因为当s字符串本身就有含有一个子串和它的倒序时,就会出错。例如ABCDFHGIJDCBA。

      • 正确做法:可以用动态规划来做,时间复杂度为O(N^2),当然还有其他算法可以让它的时间复杂度更低,例如:Manacher算法,时间复杂度为O(N).

      • 状态转移方程:

        //dp[i][j]表示字符串i-j的位置是否是回文字符串,如果是,则dp[i][j] = 1;如果不是,则dp[i][j] = 0;注意,此处不同于之前的地方是dp数组只存放1或者0的状态值。最终的记录字符串长度,需要另取一个变量。
        if(s[i] == s[j]){
            //如果此时的两个字母相同,那么该字符串是否是回文字符串取决于该字符串里面最大的那个字符串,即下标是[i+1][j-1]。如果两个字母不相同,那自然不会是回文字符串。
            dp[i][j] = dp[i + 1][j - 1];
        }else{
            dp[i][j] = 0;
        }
        //边界:
        dp[i][i] = 1,dp[i][i+1] = (s[i] == s[i+1])? 1:0
        
      • 注意:

        ①按照i和j从小到大的顺序来枚举子串的两个端点,然后更新dp[i] [j]会使得中间某几个dp值不是已经计算过的值而导致无法进行状态转移。

        ②解决办法是第一遍将长度为3的子串的dp值全部求出,第二遍通过第一遍结果计算出长度为4的子串的dp值,第三遍同理……

      • 模板代码:

        #include<cstdio>
        #include<cstring>
        using namespace std;
        int dp[110][110];
        int main(){
        	char s[110];
        	gets(s);
        	int len = strlen(s);
        	int ans = 1; //初始值为1,因为无论如何,最小回文字符串长度一定大于等于1. 
        	memset(dp, 0, sizeof(dp));
        	//dp边界
        	for(int i = 0; i < len; i++){
        		dp[i][i] = 1; //单个字母本身可以当做是回文串
        		if(i+1 < len && s[i] == s[i + 1]){
        			dp[i][i + 1] = 1;
        			ans = 2;
        		} 
        	} 
        	//dp状态转移方程
        	//此处需要外层加一个循环,因为如果只有i和j两层循环的话,让j不断增大,那么就会导致中间有一个dp的值是还未进行计算的,
        	//这时就无法继续进行状态转移。所以先要从左端点开始枚举,逐渐增大L的数值。保证每一个dp都可以顺利地转移。 
        	for(int L = 3; L <= len; L++){ //因为L是被加上的数,并不是进行循环的数,所以需要L <= len,而不是直接L < len; 
        		for(int i = 0; i + L - 1 < len; i++){
        			int j = L + i - 1;//右端点
        			if(s[i] == s[j] && dp[i + 1][j - 1] == 1){  //此处只需要进行一个状态转移方程即可,因为一开始dp数组已经
        														//全部初始化为0了,那么后序步骤就不需要再加上dp=0这个环节。 
        				dp[i][j] = 1;
        				ans = L;	//更新最大回文子串的长度 
        			} 
        		} 
        	} 
        	printf("%d", ans);
        	return 0;
        } 
        
    • DAG最长路(DAG为有向无环图)

      • 问题描述:DAG中最长路,也就是“关键路径”。着重解决两个问题:

        ①求整个DAG中的最长路径(即不固定起点和终点)

        ②固定终点,求DAG最长路。

      • 第一个问题思路:

        ①对于第一个问题,即不固定起点和终点求解最长路径。令dp[i]表示从i号顶点出发能获得的最长路径长度,这样所有dp[i]的最大值就是所求值。

        ②由于出度为0的顶点出发的最长路径长度为0,所以边界顶点的dp值都为0.但不妨将所有dp值都赋值为0.然后按照逆拓扑序列的顺序进行计算。

        ③如果要求解最长路径具体是哪一条,可以参考Dijkstra+DFS求解最短路径的方法,开一个vector容器,用来记录当前结点i的后继结点j。然后再通过dfs方式计算最优解。

        第一个问题代码实现

        vector<int> next[maxn];
        int st;
        int dp[maxn] = {0};
        int DP(int i){
            if(dp[i] > 0)return dp[i];//已经计算过
            for(int j = 0; j < n; j++){//遍历i的所有出边
                if(G[i][j] != inf){
                    int temp = DP(j) + G[i][j];//用临时变量存储,放置DP递归计算两遍
                    if(temp > dp[i]){
                        dp[i] = temp;
                        //i号顶点的后继顶点值压入到next容器中
                        next[i].push_back(j);
                        st = i;
                    }
                }
            }
            return dp[i];//返回计算完毕的dp[i]
        }
        //输出最长路径长度以及最长路径本身
        void printPath(){
            printf("最长路径长度为%d\n", dp[st]);
            printf("%d", st);
            int i = st;
            while(next[i].size() != 0){
                i = next[i][0];
                printf("->%d", next[i][0]);
            }
        }
        
      • 第二个问题思路:

        ①对第二个问题,即固定终点为T,求DAG的最长路径。令dp[i]表示从i号顶点出发到达终点T能获得的最长路径长度

        注意:第二个问题与第一个问题的区别在于有无固定终点,从代码角度来看,便是dp的初始值设置。第一个问题可以将全体顶点的dp值都设置为0;但是第二个只能将终点T的dp值设置为0,其余值不可以;原因是有些点无法到达终点T。所以应该将所有顶点先初始化为负无穷,然后将终点设置为0.

        第二个问题代码实现

        const int inf = -100000000;//注意设置为负无穷
        int DP(int i){
            if(dp[i] >= 0)return dp[i];//已经计算过
            for(int j = 0; j < n; j++){
                if(G[i][j] != inf){
                    dp[i] = max(dp[i], DP(j) + G[i][j]);
                }
            }
            return dp[i];
        }
        
    • 背包问题

      • 化简为滚动数组后,完全背包v变量正序枚举,因为dp数组是取决于dp[i]的数据,不会有覆盖的危险;01背包v变量倒序枚举,因为dp数组是取决于dp[i-1]的数据,会有覆盖的危险。

      • 01背包问题

        • 问题描述:有n件物品,每件物品的重量为w[i], 价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每件物品都只有1件。

        • 思路:

          令dp[i] [v]表示前i件物品恰好装入 容量为v的背包中所能获得的最大价值。(1 <= i <= n, 0 <= v <= V)

          ②对第i件物品的选择策略,有两种:第一种,不放第i件物品,那么问题转换为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,即dp[i-1] [v]; 第二种,放第i件物品,问题转换为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,即dp[i-1] [v-w[i]] + c[i]。

        • 状态转移方程:

          //dp[i][v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值。
          dp[i][v] = max{dp[i - 1][v], dp[i - 1][v - w[i]] + c[i]};
          (1 <= i <= n, w[i] <= v <= V)
          //边界
          dp[0][v] = 0; //意思是前0件物品放入任何容量为v的背包中只能获得价值为0
          
          //代码,时空复杂度都为O(nV)
            for(int i = 1; i <= n; i++){
                for(int v = w[i]; v <= V; v++){
                    dp[i][v] = max(dp[i-1][v], dp[i-1][v-w[i]] + c[i]);
                }
            }
          
        • 滚动数组:因为上面的状态转移方程中,计算

          dp [i] [v]时总是只用到左侧的数据,所以状态转移方程可以写为:

          注意:如果是用二维数组进行存放,v的枚举顺序逆序都可,但是如果使用一维数组(滚动数组)进行存放,v的枚举必须是逆序的;因为这样才不会将仍要进行计算的数据给覆盖掉。

           dp[v] = max(dp[v], dp[v - w[i]] + c[i])
            (1 <= i <= n, w[i] <= v <= V)
          //同时需要注意,当使用滚动数组的时候,v只能在循环中递减,而不能像之前那样递减或者递增都可以。
          //边界
              for(int v = 0; v <= V; v++){
                  dp[v] = 0;//相当于将前一维删除
              }
          
          //代码如下:
          for(int i = 1; i <=n; i++){
              for(int v = V; v >= w[i]; v--){
                  dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
              }
          }
          
        • 模板代码如下:

          //思路:读取重量与价值--> 设置边界(注意是dp[v]数组)-->  进行状态转移方程(注意使用滚动数组之后,v应该是逆序)--> 寻找dp数组中的最大值,输出
          #include<cstdio> 
          #include<algorithm>
          using namespace std;
          int w[1010], c[1010], dp[1010];
          int main(){
          	int n, V;
          	scanf("%d%d", &n, &V);
          	for(int i = 0; i < n; i++){
          		scanf("%d", &w[i]);//输入每件物品的重量 
          	} 
          	for(int i = 0; i < n; i++){
          		scanf("%d", &c[i]);//输入每件物品的价值 
          	}
          	//dp边界,注意v是[0, V]闭区间,可以取到两个端点
          	for(int v = 0; v <= V; v++){
          		dp[v] = 0;
          	} 
          	//状态转移方程
          	for(int i = 1; i <= n; i++){
          		for(int v = V; v >= w[i]; v--){
          			dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
          	}
          	} 
          //寻找dp数组中最大值
          	int max = 0; 
          for(int v = 0; v <= V; v++) {
          		if(dp[v] > max){
          		max = dp[v];
          		}
          	}
          printf("%d", max);
          	return 0;
          }
          
          
      • 完全背包问题

        • 问题描述:有n种物品,每种物品的单件重量是w[i],价值为c[i],现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件。【完全背包问题与01背包问题的区别在于,一个物品件数是有限的,也就意味着某种重量会用完;另一个是重量永远都有】

        • 分析:

          • 不放第i件物品,那么状态转移至dp[i - 1] [v],此处和01背包一致。
          • 放第i件物品,状态转移至dp[i] [v - w[i]],而不是dp [i - 1] [v - w[i]],此处和01背包不一样。因为完全背包的物品件数是无穷的,那么意味着放了第i件物品之后,仍然可以继续放第i件物品,所以状态转移的时候仍然是 i。
        • 状态转移方程如下:

          dp[i][v] = max(dp[i - 1][v], dp[i][v - w[i]] + c[i]);
          (1 <= i <= n, w[i] <= v <= V)
          //边界:
          dp[0][v] = 0;
          
          //转移成滚动数组形式下的状态转移方程
          
          dp[v] = max(dp[v], dp[v-w[i]] + c[i]);
          //边界
          dp[v] = 0;(0 <= v <= V)
          //此时的一维dp的状态转移方程,完全背包和01背包一致,但是完全背包的v是正序增加,而01背包的v是倒序减小。
          //代码如下:
          for(int i = 1; i <= n; i++){
              for(int v = w[i]; v <= V;; v++){
                dp[v] = max(dp[v], dp[v-w[i]] + c[i]);
              }
          }
          
      • 背包问题题

        • 问题描述【来自蓝桥杯第五届第十题 波动数列】:观察这个数列:
          1 3 0 2 -1 1 -2 …

        这个数列中后一项总是比前一项增加2或者减少3。

        栋栋对这种数列很好奇,他想知道长度为 n 和为 s 而且后一项总是比前一项增加a或者减少b的整数数列可能有多少种呢?

        【数据格式】
        输入的第一行包含四个整数 n s a b,含义如前面说述。
        输出一行,包含一个整数,表示满足条件的方案数。由于这个数很大,请输出方案数除以100000007的余数。

      例如,输入:
      4 10 2 3
      程序应该输出:
      2

      【样例说明】

      这两个数列分别是2 4 1 3和7 4 1 -2。

      【数据规模与约定】
      对于10%的数据,1<=n<=5,0<=s<=5,1<=a,b<=5;
      对于30%的数据,1<=n<=30,0<=s<=30,1<=a,b<=30;
      对于50%的数据,1<=n<=50,0<=s<=50,1<=a,b<=50;
      对于70%的数据,1<=n<=100,0<=s<=500,1<=a, b<=50;
      对于100%的数据,1<=n<=1000,-1,000,000,000<=s<=1,000,000,000,1<=a, b<=1,000,000。

      ​ 资源约定:
      峰值内存消耗 < 256M
      CPU消耗 < 1000ms

      【首先分析发现,数据会特别大,这个时候基本可以排除深搜完全AC的可能性,当数据过大的时候,应该往动态规划方面去想】

      【既然要往动态规划方面想,那么在动态规划的模型里面,和背包问题有点类似,但是要正确可以套用背包问题,需要找到那个不动的数值;a和b两个状态类似于01背包中的选与不选,然后延伸思考,在01背包中选与不选即+w[i]或者不加,最终是有一个固定的V值的。 于是就可推测,a和b也应该有一个固定的值。 根据数学式子变换,果不其然,可以将该式子写成:x + (x + p) + (x + 2 p) + ……+ (x + (n-1) p) = s.其中p是a和b操作,那么化简可得到+a和-b的次数和为n * (n-1) / 2.于是,顺利将此题转换为背包问题】

      【此题还有一个巧妙处理的点是利用到了滚动数组,因为更新dp数组的时候,发现只与它的前一个状态有关系,所以可以将dp数组的第一维改成大小为2,表示i或者i - 1.】

      • 具体代码如下:
       #include <iostream>
        #include <memory.h>
        #define MAXN 1100
        #define MOD 100000007
        using namespace std;
        int F[2][MAXN*MAXN]; //dp数组       
        int e = 0;
        long long n,s,a,b;
        int cnt = 0;
        void DP(int elem)
        {
            int i,j;
            memset(F,0,sizeof(F));
            F[e][0]=1;    //此处F[0][0] = 1可以理解为初始化为1种方案。 
            for(i=1;i<n;i++)
            {
                e=1-e;  //从i变为i-1或者从i-1变为i 
                for(j=0;j<=i*(i+1)/2;j++)   //j <= i*(i+1)/2就是+a与-b总共的和。 
                {
                    if(i>j)
                        F[e][j]=F[1-e][j];    //当i>j的时候,+a的方案数不增加 
                    else
                        F[e][j]=(F[1-e][j]+F[1-e][j-i])%MOD;  //当i <= j的时候,有选与不选两种选择,所以方案数应该是两者之和 
                }
           }
        }
        
        int main()
        {
          cin>>n>>s>>a>>b;
            long long i,t;
            DP(n*(n-1)/2);
            for(i=0; i<=n*(n-1)/2; i++)
            {
                t = s - i*a + (n*(n-1)/2-i)*b;  //这里t为什么要等于后面那一串啊??还有为什么t要是n的整数倍??t的含义是什么? 
                if(t%n==0)
                 cnt = (cnt+F[e][i])%MOD;
            }
            printf("%d",cnt);
        return 0;
        }
      
      

  • 动态规划总结:

    • 各类题型:
      • 最大连续子序列和
        • dp[i]表示以A[i]作为末尾的连续序列的最大和
      • 最长不下降子序列(LIS)
        • dp[i]表示以A[i]作为末尾的最长不下降子序列长度
      • 最长公共子序列(LCS)
        • dp[i] [j]表示字符串A的i号位和字符串B的j号位之间的LCS长度
      • 最长回文子串
        • dp[i] [j]表示s[i]至s[j]所表示的子串是否是回文子串,值为0或者为1
      • 数塔DP
        • dp[i] [j] 表示从第i行到第j个数字出发的到达最底层的所有路径上所能得到的最大和。
      • DAG最长路
        • dp[i]表示从第i行第j个数字出发的到达最底层的所有路径上所能获得的最大和
      • 01背包
        • dp[i] [v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值。
      • 完全背包
        • dp[i] [v]表示前i件物品恰好装入容量为V的背包中所能获得的最大价值
    • 总结共同点:
      • 一般来说,子序列是可以不连续的,而子串一定要是连续的。
      • 当题目与字符串或者序列(记为A)有关时,可以考虑把状态设计成下面两种形式,然后根据端点特点去考虑状态转移方程
        • 1、令dp[i]表示以A[i]结尾(或者开头)的xxxxx
        • 2、令dp[i] [j]表示A[i] 到A[j]之间的xxxxxx
      • 后几类发现,它们的状态设计都包含了某种“方向”的意思。数塔dp中设计为从点(i, j)出发到最底端的最大和,DAG设计为从i号顶点出发的最长路,背包问题中则设计成dp[i] [v]表示前i件物品恰好放入容量为V的背包中能获得的最大价值。 所以,分析题目之中的状态需要几维来表示,然后对其中的每一维采取下面的某一个表达:
        • ①恰好为i
        • ②前i
        • 在每一维的含义设置完毕之后,dp数组的含义就可以设置成“令dp数组表示恰好为i(或者前i)……的xxxx(对问题的描述)。然后再通过端点的特点去考虑状态转移方程。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦想总比行动多

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值