Dynamic Programming

1.斐波那契数列

递归是自顶向下

dp是自底向上。从f(0)=0(作为pre)和f(1) =1(作为cur)开始,不断相加向上迁移运算状态最终得到n。

#include <bits/stdc++.h>
using namespace std;

int T;
long long n;
const int MOD = 1e9 + 7;

unsigned long long fib(long long n){
	 if( n==0 ){
		return 0;
	}
	else{
		unsigned long long pre = 0; // f(0) = 0
		unsigned long long cur = 1; // f(1) = 1
		for(int i=2; i<=n; i++){
			unsigned long long next = (pre+cur)%MOD; // f(2) = f(0) + f(1) 
                                                        // f(3) = f(1) + f(2)
			pre = cur; 
			cur = next;
		}
		return cur;
	}
}


int main(){
	scanf("%d", &T);
	for(int i=0; i<T; i++){
		scanf("%lld",&n);
		printf("%lld\n", fib(n));
	}
	return 0;
}

2.数字三角形

一层只能走一个顶点,从第一层开始走走到底部,求最大的顶点和。 下标从1开始

【自底向上思路】

1.dp[i][j]的含义:从最下层走到三角形第i层第j个元素时,所具有的最大值。要获得最终路径值即dp[1][1]。

2.初始化:最底层每个元素的dp都等于它本身(dp[m][j] = a[m][j])

3.状态迁移:从倒数第二层开始走,对于每层第j个,它可以到达下一层的第j个和j+1个,所以在这两个顶点里选一个最大的,加到它本身,在加上自身的值。直到到达第一层。

#include <bits/stdc++.h>
using namespace std;

int m,v;
int T;
int d[52][52], a[52][52];

// bottom-up
int solve(){
	for(int j=1; j<=m; j++) // 初始化最后一层
		d[m][j] = a[m][j];
	for(int i=m-1; i>=1; i--){
		for(int j=1; j<=i; j++){
			d[i][j] = a[i][j] + max( d[i+1][j], d[i+1][j+1]);
		}
	}
	return d[1][1];
}

int main(){
	scanf("%d", &T);
	while(T--){
		memset(d, 0, sizeof(d));
		memset(a, 0, sizeof(a));
		scanf("%d", &m);
		//int num = m*(m+1) / 2;
		for(int i=1; i<=m; i++){
			for(int j=1; j<=i; j++){
				scanf("%d", &a[i][j]);
			}
			
		}
		printf("%d\n", solve());
	}
	
}

 3.完全背包

只看完全背包和01背包就够 

描述: Tom是一名卡车司机。他将商品从生产地运输到销售地,然后根据所运输货物的总重量获得报酬。每种商品都有其重量wi和价值vi,Tom在运输一件这种类型的商品后可以获得这个价值。Tom的卡车的载重量为W。你能写一个程序来帮助Tom赚取最大的金额吗?注意,每种商品的数量是无限的

输入: 输入以整数T(T < 80)开头,表示测试用例的数量。然后是T个测试用例。对于每个测试用例,第一行是两个整数,商品类型的数量N(1 <= N <= 1000)和Tom卡车的载重量W(1 <= W <= 1000)。下一行是N个整数,表示Tom在运输第i种商品时可以获得的价值vi(1 <= i <= N)。每个测试用例的最后一行是N个整数,表示第i种商品的重量wi(1 <= i <= N)。

输出: 对于每个测试用例,输出一个整数,表示Tom可以赚取的最大金额。

输入样例 1 

1
5 10
1 2 3 4 5
5 4 3 2 1

输出样例 1

50

 【思路】

1.dp[j]含义:容量为j的背包,所具有的最大价值

2.初始化:都是0

3.状态迁移:遍历每个物品,判断其是否加入容量变化范围是[weight[],  W]的背包。

为什么背包容量要从小到大遍历 

在计算dp[j]时,我们需要用到dp[j-weight[i]]的值。如果我们按照从小到大的顺序遍历物品,则当计算dp[j]时,如果dp[j-weight[i]]已经添加了i,则会将第i个物品再次添加到背包中,但完全背包本来就是可以将物品放多次的只要保证价值最大就行。

#include <bits/stdc++.h>
using namespace std;

int weight[1005];
int value[1005];
int dp[1005];

int T,N,W;

int completePack(){
	memset(dp, 0, sizeof(dp));
	for(int i=0; i<N; i++){
		for(int j=weight[i]; j<=W; j++){
			dp[j] = max( dp[j], dp[j-weight[i]] + value[i]);
		}
	}
	return dp[W];
}

int main(){
	scanf("%d", &T);
	while(T--){
		scanf("%d %d", &N, &W);
		for(int i=0; i<N; i++){
			scanf("%d", &value[i]);
		}
		for(int i=0; i<N; i++){
			scanf("%d", &weight[i]);
		}
		printf("%d\n", completePack());
	}
}

 4.0/1背包

dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

那么可以有两个方向推出来dp[i][j],

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

遍历每个物品,判断其是否要放入容量不同的背包。(虽然最后只用得到容量为bagweight的dp,但是状态必须逐渐迁移)

// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

    }
}

【思路】一维数组

为什么背包容量需要从大到小遍历

在计算dp[j]时,我们需要用到dp[j-weight[i]]的值。如果我们按照从小到大的顺序遍历物品,则当计算dp[j]时,如果dp[j-weight[i]]已经添加了i,则会将第i个物品再次添加到背包中,这样就违背了0-1背包问题中每个物品只能选择一次的条件。

通过从大到小遍历物品,我们可以确保在计算dp[j]时,dp[j-weight[i]]还没有被处理,一定没有放物品i。这样,每个物品只会被添加一次,保证了问题的解是符合0-1背包问题的要求的。

4.变体0/1背包重量为浮点数。

每个物品重量为浮点数(爆炸概率),价值为整数。

龙的珍宝
描述

汤姆去了龙窝,想要得到龙的宝藏。龙说:“我把我的宝贝放在‘N’盒子里。对于每个“i”宝盒,当你打开它时,它有“pi”爆炸的概率。你可以拿走你所有的宝藏。”但龙不知道,先知告诉汤姆,如果他打开的所有宝盒的爆炸概率总和不超过“P”,他就能活下来。汤姆想知道他能得到多少宝物。


输入

输入的第一行给出了T (0<T<=100),测试用例的数量。对于每个测试用例,第一行给出了一个浮点数P (0.0<=P<=1.0), Tom打开的所有宝箱的爆炸概率之和不能超过,以及一个整数N (0<N<=100),即Dragon给Tom的箱子数量。然后按N行排列,其中第i行给出一个整数Mi (0<Mi<=100)和一个浮点数pi (0.0<=pi<=1.0),表示盒子i中有Mi宝藏,其爆炸概率为pi。


输出

对于每个测试用例,输出一行表示Tom可以获得的最大宝藏

输入样例 1 

3
0.08 3
2 0.03
2 0.03
3 0.05
0.14 3
1 0.03
2 0.02
3 0.05
0.04 3
1 0.01
2 0.02
3 0.03

输出样例 1

5
6
4

【思路】

1.dp[i]的含义:物品重量是浮点数,意味着背包重量也是浮点数。所以不能以重量为dp数组的值。dp[i]表示价值为i时,打开所有放入背包的盒子的不爆炸概率。

2.初始化:dp[0]表示价值为0,即不放任何物品,dp[0]的不爆炸概率一定是1。其余全是0。

3.状态迁移:与01背包遍历方式一样。外层遍历物品,内层从大到小遍历价值。dp[j]判断价值为i的物品放不放,判断标准是相较于不放i的不爆炸概率会不会增加。(dp[]当然是越大越好)

#include <bits/stdc++.h>
using namespace std;

int T, N;
double P;

double p[105];
int value[105];
double dp[105*105];

int pack01(int sum){
	// dp[i]是价值为i时不爆炸概率
	dp[0] = 1;
	for(int i=1; i<=N; i++){
		for(int j=sum; j>=value[i]; j--){
			dp[j] = max( dp[j], dp[j-value[i]]*(1-p[i]));
			//dp[j] = max( dp[j], dp[j-value[i]]+p[i]);
		}
	}
	// 遍历dp数组,找到大于【可接受不爆炸概率1-P】的最大价值
	for(int i=sum; i>=0; i--){
		if( dp[i] > 1-P ){
			return i;
		}
	}
	
}
int main(){
	scanf("%d", &T);
	while( T-- ){
		scanf("%lf %d", &P, &N);
		memset(dp, 0, sizeof(dp));
		memset(value, 0, sizeof(value));
		memset(p, 0, sizeof(p));
		int sum = 0;
		for(int i=1; i<=N; i++){
			scanf("%d %lf", &value[i], &p[i]);
			sum += value[i];
		}
		printf("%d\n", pack01(sum));
	}
}

5.最长递增子序列和

求元素和,每个元素都是正数,取数不必连续但必须递增。

【思路】

1.dp[i]含义:以i为结尾的最大递增子序列和

2.初始化:dp[i] = a[i]

3.状态迁移:外层从第二个元素开始遍历每一个元素,内层遍历每个元素之前整个序列,如果前面的数小于a[i],满足递增序列,需要前面的数的dp + a[i]。然后再和当前i结尾的dp比较是不是会让最大和增加(肯定是增加的,因为外层从小到大,所以每个数都是初次处理的,dp[i]都是初始化的值)。选择更大的dp[i]。

#include <bits/stdc++.h>
using namespace std;

//dp[i] 以a[i]为结尾的最大递增子序列和
// dp[i] = max(dp[j]+a[i], dp[i])

int a[1004];
long long dp[1004];
int N;
long long solve(){
	long long ans = 0;
	for(int i=1; i<N; i++){
		for(int j=0; j<i; j++){
			if( a[i]>a[j] ) // 满足递增 再增加
				dp[i] = max(dp[j]+a[i], dp[i]);
		}
		
	}
	for(int i=0; i<N; i++){
		if( dp[i]>ans )
			ans = dp[i];
	}
	return ans;
}

int main(){
	while(scanf("%d", &N)!=EOF){
		memset(dp, 0, sizeof(dp));
		for(int i=0; i<N; i++){
			scanf("%d", &a[i]);
			dp[i] = a[i];
		}
		printf("%lld\n", solve());
	}
	
}

最长递增序列个数

每个元素都是正数,取数不必连续但必须递增。

【思路】

1.dp[i]含义:以i为结尾的最大递增子序列个数

2.初始化:dp[i] = 1

3.状态迁移:外层遍历每一个元素,内层遍历每个元素之前整个序列,如果前面的数小于a[i],满足递增序列,需要前面的数的dp + 1。然后再和当前i结尾的dp比较是不是会让最大长度增加(肯定是增加的,因为外层从小到大,所以每个数都是初次处理的,dp[i]都是初始化的值)。选择更大的dp[i]。

#include <bits/stdc++.h>
using namespace std;

int a[1004];
long long dp[1004];
int n;

long long solve(){
	
	for(int i=1; i<n; i++){
		for(int j=0; j<i; j++){
			if( a[j] < a[i] ){
				dp[i] = max( dp[i], dp[j]+1);
			}
		}
	}
	long long max = 0;
	for(int i=0; i<n; i++){
		if( dp[i] > max ){
			max = dp[i];
		}	
	}
	return max;
}

int main(){
	while( scanf("%d", &n)!=EOF ){
		memset(dp, 0, sizeof(dp));
		for(int i=0; i<n; i++){
			scanf("%d", &a[i]);
			dp[i] = 1;
		}
		
		printf("%lld\n", solve());
	}
}

【思路】

绳子切割只能是整数,所以所有绳段长度和一定等于总绳长,不会有剩余段。 

1.dp[i]含义:总长为i的绳子所具有的最大价值。

2.初始化:dp[1] = value[1] 其余全是0

3.状态迁移:遍历从1到n所有长度,对于每种长度,遍历其所有切割情况,从中选择价值最大的那一次切割情况。

如何遍历所有切割情况?对于长度i的绳子,可以在长度1,2...i的位置切割,每个切割点只需要判断是否切割。如果在长度j的位置切下这一刀,0到j长度的绳段价值value[j] + j到i长度绳段的最大价值dp[j-i]是否比最大值大。

#include <bits/stdc++.h>
using namespace std;

int n;
int value[10005];
int dp[1005];

int main(){
	while( scanf("%d", &n)!=EOF ){
		for(int i=1; i<=n; i++){
			scanf("%d", &value[i]);
			dp[i] = 0;
		}
		dp[1] = value[1];
		
		for(int i=1; i<=n; i++){
			int max = -1;
			for(int j=1; j<=i; j++){ // 在j处切割
				if( value[j]+dp[i-j]>max ){ // 判断这一刀切不切
					max = value[j]+dp[i-j];
				}
			}
			dp[i] = max;
		}
		
		printf("%d", dp[n]);
	}
}

 6.最长公共子序列

最长公共子序列(Longest Common Subsequence)是指在两个序列中找到最长的公共子序列的问题。给定两个序列,可以是字符串、数组或其他类型的序列,要求找到一个新的序列,该序列是原序列的子序列,且同时是两个序列的子序列,且具有最长的长度。

具体而言,给定序列X和Y,最长公共子序列LCS(X, Y)是一个新的序列,它在X和Y中都是子序列,并且具有最长的长度。子序列是通过从原序列中删除一些元素(可以不连续)而得到的新序列。

例如,对于序列X="ABCD"和Y="ACDF",它们的最长公共子序列是LCS(X, Y) = "ACD",长度为3。这意味着在序列X和序列Y中,我们可以找到一个长度为3的子序列,该子序列同时也是两个序列的子序列。

// LCS
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn = 1000 + 5;

char A[maxn], B[maxn];
int d[maxn][maxn];

int max(int x, int y){
	return x>y ? x : y;
}

int LCS(const char* A, int n, const char* B, int m) {
  memset(d, 0, sizeof(d));
  for(int i = 1; i <= n; i++)
    for(int j = 1; j <= m; j++) {
      if(A[i-1] == B[j-1]) d[i][j] = d[i-1][j-1] + 1;
      else d[i][j] = max(d[i][j-1], d[i-1][j]);
    }
  return d[n][m];
}

int main() {
  while(fgets(A, maxn, stdin) != NULL) {
    fgets(B, maxn, stdin);
    //The reason of "-1" is that fgets() inputs a carriage return '\r'
    printf("%d\n", LCS(A, strlen(A)-1, B, strlen(B)-1));
  }
  return 0;
}

7.0/1背包简单变体

War 点燃烽火台的最短时间

描述

A war begins, there are n beacon towers (烽火台) connected in a straight line. The ith tower is located at axis (轴线) i, with Ai monitoring range (监视范围), that is, the ith tower's monitoring range is [max(0,i-Ai), i]. A tower will light its beacon if there are enemies in its monitoring range or if the soldier in it sees another tower's beacon is lit. However, lighting a beacon needs time, and the time for each tower's soldier to light the beacon may not be the same. The time for the ith tower's soldier to light the beacon is Ti. General W, the commander (指挥官W将军), wants to know how long it will take for each tower to light its beacon if there are enemies at axis 0.

输入

The first line contains an integer n (1<=n<=100000) — the number of beacon towers.

The second line contains n integers, and the ith integer is Ti (1<=Ti<=10^9).

The third line contains n integers, and the ith integer is Ai ( 1<=Ai<=10).

输出

Output n lines. The ith line contains a number representing the ith tower's light time.

输入样例 1 

5
3 1 1 2 4
1 2 2 2 3

输出样例 1

3
1
2
3
5

提示

20分。

The 1st beacon tower receives enemy message from axis 0 in Time 0 and lights the beacon in Time 3.

The 2nd beacon tower receives enemy message from axis 0 in Time 0 and lights the beacon in Time 1.

The 3rd beacon tower receives enemy message from axis 2 in Time 1 and lights the beacon in Time 2.

The 4th beacon tower receives enemy message from axis 2 in Time 1 and lights the beacon in Time 3.

The 5th beacon tower receives enemy message from axis 2 in Time 1 and lights the beacon in Time 5.

Simple DP

【思路】对于每一个观测范围内的塔,只有选与不选两种情况(0/1背包)。但是由于用选一个(这也是思考过程中的难点所在),所以退化成了遍历可观测范围内的所有塔,选dp[]最小的塔。 很常见的求最小值问题。

#include <bits/stdc++.h>
using namespace std;

int n;
long long t[100005];
int a[100005];
long long dp[100005];

int main(){
	scanf("%d", &n);
	for(int i=1; i<=n; i++){
		scanf("%lld", &t[i]);
	}
	int Ai;
	for(int i=1; i<=n; i++){
		scanf("%d", &Ai);
		int start = max(0, i-Ai);
		a[i] = start;
	}
	dp[0] = 0;
	
	for(int i=1; i<=n; i++){
		long long m = numeric_limits<long long>::max();
		for(int j=i-1; j>=a[i]; j--){
			m = min(dp[j], m);
		}
//		for(int j=i; j>=a[i]; j--){
//			dp[j] = min(dp[j], dp[j-a[i]]+t[j]);
//		}
		dp[i] = t[i] + m;
	}
	
	
	for(int i=1; i<=n; i++){
		printf("%lld\n", dp[i]);
	}
}

8. Dynamic programming on DAG(有向无环图上的动态规划)

在计算机科学中,DAG(有向无环图)是一种图形结构,其中顶点之间的有向边形成一个无环的有向路径。DAG常用于建模许多问题,如任务调度、依赖关系和计算优化等。

动态规划(Dynamic Programming)是一种算法设计技术,用于解决具有重叠子问题性质的问题。它将问题分解为一系列重叠的子问题,并使用递归或迭代的方式,将解决每个子问题的结果存储起来,避免重复计算。

动态规划在DAG上的应用称为"DAG上的动态规划"(Dynamic Programming on DAG)。它在DAG中使用动态规划的思想,通过计算每个顶点的最优值来解决与顶点相关的问题。

DAG上的动态规划通常使用拓扑排序(Topological Sort)来确定计算的顺序。拓扑排序是一种对有向无环图进行排序的方法,它保证了在进行动态规划计算时不会出现依赖关系的问题。

具体而言,DAG上的动态规划包括以下步骤:
1. 执行拓扑排序,确定计算的顺序。
2. 按照拓扑排序的顺序,依次计算每个顶点的最优值。
3. 对于每个顶点,根据其依赖的其他顶点的最优值,计算自身的最优值。
4. 最终得到整个DAG的最优解。

DAG上的动态规划常用于解决一些与图相关的问题,例如最长路径问题、最短路径问题、最大权重路径问题等。通过利用DAG的拓扑排序和动态规划的思想,可以高效地解决这些问题,并避免不必要的重复计算。

输入从一行开始,一行包含盒子的数量n。这一行后面是n行,一行一个盒子,每个盒子的x和y。
输出一行上最长嵌套字符串的长度,然后在下一行上按顺序输出组成该字符串的框的列表。应该首先列出嵌套字符串的“最小”或“最里面”的框,其次列出下一个框(如果有),依此类推。

 【思路】首先要建立有向图。之后可以用dfs(见DFS那章)或者dp。

1.dp[i]含义:以i为起点的最长路径长度。

2.初始化:都是0

3.状态迁移:遍历每个盒子/顶点,获得他们的dp[]。对于dp[i],遍历i的所有邻接顶点,获得最大的长度(dp数组最大),然后+1。dp数组是长度,拓扑路径需要递归获取。

#include<stdio.h>
#include<string.h>
#define MAXN 1010
int n, G[MAXN][MAXN]; // DAG
int x[MAXN], y[MAXN], d[MAXN]; // d[i] means the length of the longest path from the vertex i

int max(int a, int b){
	return a>b ? a : b;
}
int dp(int i) {
  int& ans = d[i];  // to simplify the code
  if(ans > 0) return ans;
  ans = 1;
  for(int j = 1; j <= n; j++) if(G[i][j]) ans = max(ans, dp(j)+1);
  return ans;
}

void print_ans(int i) {
  printf("%d ", i);
  for(int j = 1; j <= n; j++) if(G[i][j] && d[i] == d[j]+1) {
    print_ans(j);
    break;
  }
}

int main() {
  int i, j, ans, best;
  scanf("%d", &n);
  for(i = 1; i <= n; i++) {
    scanf("%d%d", &x[i], &y[i]);
    if(x[i] > y[i]) {          // x<y x存短边 y存长边
      int t = x[i]; x[i] = y[i]; y[i] = t;
    }
  }
  // 建立有向图
  memset(G, 0, sizeof(G));
  for(i = 1; i <= n; i++)
    for(j = 1; j <= n; j++)
      if(x[i] < x[j] && y[i] < y[j]) G[i][j] = 1;  // generate the DAG

  ans = 0;
  for(i = 1; i <= n; i++)
    if(dp(i) > ans) {
      best = i;
      ans = dp(i);
    }
  printf("%d\n", ans);
  print_ans(best);
  printf("\n");
  return 0;
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值