【C/C++算法】动态规划(线性dp、区间dp)

一、线性dp

数字三角形

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

输入格式
第一行包含整数 n,表示数字三角形的层数。

接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。

输出格式
输出一个整数,表示最大的路径数字和。

数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000

输入样例:

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

输出样例:

30

思路
状态表示:用f[i][j]来表示状态,其中i表示第几行(从1开始),j表示第几斜列(从1开始),斜列是指从右上到左下的列,比如该题中7、3、8、2、4在第1斜列。则f[i][j]表示,从起点到a[i][j]的所有情况集合中,使得路径和最大的一种情况,其f[i][j]的值就是该路径和的值
状态计算:在该三角中,我们可以将f[i][j]的计算划分来源,分为从a[i][j]左上f[i-1][j-1]而来和从a[i][j]右上f[i-1][j]而来
所以综上:f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j])
在遍历计算后,遍历最后一行取最大值即可

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

const int N = 500;
const int INF = 0x3f3f3f3f;

int main(){
    int n,res;
    int w[N][N],f[N][N];

    scanf("%d",&n);
    memset(f,-0x3f,sizeof(f));

    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            scanf("%d",&w[i][j]);
        }
    }

    f[1][1] = w[1][1];
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++){
            f[i][j] = max(f[i-1][j-1]+w[i][j],f[i-1][j]+w[i][j]);
        }
    }

    res = -INF;
    for(int i=1;i<=n;i++)
        res = max(res,f[n][i]);
    printf("%d",res);

    return 0;
}

摘花生

Hello Kitty想摘点花生送给她喜欢的米老鼠。

她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。

地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。

Hello Kitty只能向东或向南走,不能向西或向北走。

问Hello Kitty最多能够摘到多少颗花生。

在这里插入图片描述
输入
每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C。
输出格式
对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数。

输入样例:

2 2
1 1
3 4

输出样例:

8

输入样例:

2 3
2 3 4
1 6 5

输出样例:

16
#include <iostream>
#include <cstring>
using namespace std;

const int N = 105;

int main(){
    int r,c;
    int w[N][N],f[N][N];

    scanf("%d %d",&r,&c);
    memset(f,0,sizeof(f));

    for(int i=1;i<=r;i++){
        for(int j=1;j<=c;j++){
            scanf("%d",&w[i][j]);
        }
    }

    for(int i=1;i<=r;i++){
        for(int j=1;j<=c;j++){
            f[i][j] = max(f[i][j-1]+w[i][j],f[i-1][j]+w[i][j]);
        }
    }
    printf("%d",f[r][c]);

    return 0;
}

最长上升子序列

问题描述
给定一个长度为 N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式
第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
1≤N≤1000,
−109≤数列中的数≤109

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

算法
子序列并非指在连续的单元形成的子序列,可以不连续
状态表示:用f[i]表示状态,f[i]是指所有以第i个数结尾的上升子序列的集合中,取最长上升子序列,f[i]的值就是该最长上升子序列的长度值
状态计算:我们可以从上一个元素是什么入手,即从倒数第二个元素是什么入手,f[i]=max(f[j]+1),j=0,1,2,…,i-1
遍历计算后,在遍历一遍所有f[i]取最大值

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

const int N = 105;

int main(){
    int n,res;
    int a[N],f[N];
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&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);
        }
    }
    for(int i=1;i<=n;i++)
        res = max(res,f[i]);
    printf("%d\n",res);
    return 0;
}

怪盗基德的滑翔翼

怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。

而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。

有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。

不得已,怪盗基德只能操作受损的滑翔翼逃脱。

假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。

初始时,怪盗基德可以在任何一幢建筑的顶端。

他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。

因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。

他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。

请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?

输入格式
输入数据第一行是一个整数K,代表有K组测试数据。

每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。

输出格式
对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。

数据范围
1≤K≤100
1≤N≤100
0<h<10000

输入样例:

3
8
300 207 155 299 298 170 158 65
8
65 158 170 298 299 155 207 300
10
2 1 3 4 5 6 7 8 9 10

输出样例:

6
6
9

思路
初始时,可以选择任意一个点进行下降,可以选择一个方向逃跑(线性:左或者右),中途不改变方向
求(1~n):以a[i]结尾的最长上升子序列
求(n~1): 以a[i]结尾的最长上升子序列
最后,取最大值

#include<iostream>
#include<cstring>

using namespace std;

const int N = 110;

int t,n;
int a[N];
int f[N],g[N];

int main() {
    cin >> t;
    while(t--){
        cin >> n;
        memset(a,0,sizeof a);
        memset(f,0,sizeof f);
        memset(g,0,sizeof g);

        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);
                }
            }
        }
        for(int i = n; i; i -- ){
            g[i] = 1;
            for(int j = n; j > i; j -- ){
                if(a[j] < a[i]){
                    g[i] = max(g[i],g[j] + 1);
                }
            }
        }
        int res = 0;
        for(int i = 1; i <= n; i ++ ) res = max(res,max(f[i],g[i]));
        cout<<res<<endl;
    }
    return 0;
}

最大上升子序列的和

一个数的序列 bi,当 b1<b2<…<bS 的时候,我们称这个序列是上升的。

对于给定的一个序列(a1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,…,aiK),这里1≤i1<i2<…<iK≤N。

比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。

这些子序列中和最大为18,为子序列(1,3,5,9)的和。

你的任务,就是对于给定的序列,求出最大上升子序列和。

注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。

输入格式
输入的第一行是序列的长度N。

第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

输出格式
输出一个整数,表示最大上升子序列和。

数据范围
1≤N≤1000

输入样例:

7
1 7 3 5 9 4 8

输出样例:

18
#include<iostream>
#include<cstring>

using namespace std;

const int N = 110;

int n;
int a[N],f[N];

int main() {
    cin >> n;
    memset(a,0,sizeof a);
    memset(f,0,sizeof f);

    for(int i = 1; i <= n; i ++ ) cin >> a[i];
    for(int i = 1; i <= n; i ++ ){
        f[i] = a[i];
        for(int j = 1; j < i; j ++ ){
            if(a[j] < a[i]){
                f[i] = max(f[i],f[j] + a[i]);
            }
        }
    }

    int res = 0;
    for(int i=1;i<=n;i++)
        res = max(res,f[i]);
    cout<<res<<endl;
    return 0;
}

二、区间dp

在合并区间时,一般会有消耗(根据题意去计算),状态转移方程就可以表示成:

dp[i][j] = min(dp[i][j], dp[i,k] + dp[k+1][j] + 合并区间的消耗 ) (k是区间分割点)

模板代码如下:

for (int len = 2; len <= n; len++) {//先枚举区间长度
	for (int i = 1; i+len-1 <= n; i++) {//再枚举区间左端点,左端点加区间长度为右端点,不能大于n
		int j = i+len-1;	//区间右端点
		for (int k = i; k < j; k++) {	//枚举区间分割点
			dp[i][j] = Math.min(dp[i][j], dp[i][k]+dp[k+1][j]+合并区间的消耗);
		}
	}
}

模板2:
在这里插入图片描述

石子合并

题目描述
设有 N堆石子排成一排,其编号为 1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2堆,代价为 4,得到 4 5 2, 又合并 1、2堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入格式
第一行一个数 N表示石子的堆数 N。

第二行 N个数,表示每堆石子的质量(均不超过 1000)。

输出格式
输出一个整数,表示最小代价。

数据范围
1≤N≤300

输入样例:

4
1 3 5 2

输出样例:

22

解题思路:

关键点:最后一次合并一定是左边连续的一部分和右边连续的一部分进行合并

状态表示:f[i][j]表示将 i 到 j 这一段石子合并成一堆的方案的集合,属性 Min

状态计算: (1) i<j 时,f[i][j]=min f[i][k]+f[k+1][j]+s[j]−s[i−1] (2)i=j 时,

f[i][i]=0(合并一堆石子代价为 0)

问题答案: f[1][n]

所有的区间dp问题枚举时,第一维通常是枚举区间长度,并且一般 len = 1 时用来初始化,枚举从 len = 2 开始;第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 110,INF = 1e9;

int n;
int s[N];
int f[N][N];

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) scanf("%d", &s[i]);

	for(int i = 1; i <= n; i ++ ) s[i] += s[i-1]; //计算前缀和

	for(int len = 2; len <= n; len ++ ) //遍历每种长度
		for(int i = 1; i + len - 1 <= n; i ++ ){ //遍历每种起点
			int l = i, r = i + len - 1;
			f[l][r] = INF;//初始化,全局变量初始值为0

			for(int k = l; k < r; k ++ )
				f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l-1]);
		}

	printf("%d\n",f[1][n]);

	return 0;
}

Multiplication Puzzle

Time Limit: 1000MS Memory Limit: 65536K
Total Submissions: 20384 Accepted: 12432
Description

The multiplication puzzle is played with a row of cards, each containing a single positive integer. During the move player takes one card out of the row and scores the number of points equal to the product of the number on the card taken and the numbers on the cards on the left and on the right of it. It is not allowed to take out the first and the last card in the row. After the final move, only two cards are left in the row.

The goal is to take cards in such order as to minimize the total number of scored points.

For example, if cards in the row contain numbers 10 1 50 20 5, player might take a card with 1, then 20 and 50, scoring
10150 + 50205 + 10505 = 500+5000+2500 = 8000

If he would take the cards in the opposite order, i.e. 50, then 20, then 1, the score would be
15020 + 1205 + 1015 = 1000+100+50 = 1150.
Input

The first line of the input contains the number of cards N (3 <= N <= 100). The second line contains N integers in the range from 1 to 100, separated by spaces.
Output

Output must contain a single integer - the minimal score.
Sample Input

6
10 1 50 50 20 5
Sample Output

3650

在这里插入图片描述

#include<iostream>
#include<cstring>

using namespace std;

const int N = 110,INF = 1e9;

inline int gi()
{
    int f = 1, x = 0; char c = getchar();
    while (c < '0' || c > '9') { if (c == '-') f = -1; c = getchar();}
    while (c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar();}
    return f * x;
}

int dp[N][N], a[N];

int main()
{
    int n,m;
	memset(dp, 0, sizeof(dp));//初始化
	n = gi();
	for (int i = 1; i <= n; i++) a[i] = gi();
	for (int i = 3; i <= n; i++)//枚举区间长度
	{
		for (int j = 1; j + i <= n + 1; j++)//枚举起点
		{
			int k = j + i - 1;//终点
			dp[j][k] = 1000000007;//当前区间的dp数组初始化
			for (int l = j + 1; l <= k - 1; l++)//枚举分割点
			{
				dp[j][k] = min(dp[j][k], dp[j][l] + dp[l][k] + a[j] * a[k] * a[l]);//进行状态转移
			}
		}
	}
	printf("%d\n", dp[1][n]);//最后输出答案
	return 0;//结束
}

如果本期文章对你有帮助,欢迎点赞+关注,支持一下,谢谢!
请添加图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值