动态规划的概述

本文介绍了三种方法解决楼梯问题,包括深度优先搜索、排列组合以及递归和动态规划。接着详细解释了动态规划的两个要素——最优子结构和无后效性,并展示了四个具体问题的解决方案,涉及最长上升子序列、最短路径、最长公共子序列和最长回文子串的计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、导入

问题:楼梯一共有 n 阶,上楼可以一步上一阶,也可以一步上二阶。请求出走到第 n 阶共有多少种不同的走法。1 \leq n\leq 50 

方法一:

搜索,我们写一个dfs,依次枚举每一步向上走多少台阶,最后统计有多少可行的方案。

方法二:排列组合

我们先把表示方法简化:

只用1和2的序列表示从下到上每一步分别走了多少台阶。

2 2 2 2 2表示一共走了五步,每次走两级台阶,这是一种有效的走法

1 1 1 1 2 2一共走了7步,前四步走一级台阶后三步走二级台阶,这也是有效的走法

接着枚举一共几步走了2级台阶,就能算出有几步走了1级台阶,最后可以用组合数学算。

10 = 10 * 1(10个1,共C^{_{10}^{10}} = 1种)

     + 8 * 1 + 2 * 1(8个1,1个2,共C^{_{9}^{8}} = 9 种)

     + 6 * 1 + 2 * 2 (6个1,2个2,共C^{_{8}^{6}} = 28种)

     + 4 * 1 + 3 * 2(4个1,3个2,共C_{7}^{4} = 35种)

     + 2 * 1 + 4 * 2 (2个1, 4个2,共C^{_{6}^{2}} = 15种)

     + 5 * 2 (5个2,共1种)

共计1 + 9 + 28 + 35 + 15 == 89种

方法三:递归

考虑最后一步的情况,我们只需从第9级或者第8级走过去。不管走到第8级和第9级的过程,如果我们知道走第8级的走法有x种,走第9级的走法有y种,那么走到第10级的走法一共就有x + y种

我们把走到i级台阶的走法用f(i)表示,有f(10) = f(9) + f(8)

同理有f(9) = f(7) + f(8), f(8)  = f(6) + f(7)

对于任意的n >= 2, f(n) = f(n - 1) + f(n - 2)(其实就是一个斐波拉契数列)

如果不递归,而是从头开始推,从小到大,一个一个计算出f(i)

这就是最简单的递归了

动态规划的两个要求

最优子结构:大问题的(最优)解可以由小问题的(最优)解推出,在这个题中,大问题f(n)的解可以由小问题f(n - 2) 和 f(n - 1) 的解推出。注意在问题拆解的过程中不能无限递归。

无后效性:未来与过去无关,一旦得到了一个小问题的解,如何得到他的过程不影响大问题的求解。在这个题中,要求出f(n),只需要知道f(n - 1) 和 f(n - 2)的值,而他们如何得到的已经不是关键了。

动态规划的两个要素:

状态:求解过程进行到哪一步,可以理解为一个子问题。

转移:从一个状态(小问题)的(最优)解推导出另一个状态(大问题)的(最优)解的过程。

代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll; 
ll n, f[51];
int main() {
	scanf("%lld", &n);
	f[0] = f[1] = 1;
	for (ll i = 2; i <= n; ++i)
		f[i] = f[i - 1] + f[i - 2];
	printf("%lld\n", f[n]);
	return 0;
}

第二题:

给定 n 个点 m 条边的有向图,每条边有个边权,代表经过这条边需要花费的时间,我们只能从编号小的点走到编号大的点,问从 1 号点走到 n号点最少需要花费多少时间?

状态:用f[i]表示从1号顶点走到i号顶点需要花费多少时间。

转移:假设我们已经知道了f[x]的值,并且存在一条x到y的代价为z的边,那么有

f[y] = min(f[y],f[x]+z)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll; 
ll n, m, f[1001], a[1001][1001];
int main() {
	scanf("%lld%lld", &n, &m);
	memset(f, 127, sizeof(f));
	memset(a, 127, sizeof(a));
	for (ll i = 1; i <= m; ++i) {
		ll u, v, w;
		scanf("%lld%lld%lld", &u, &v, &w);
		a[u][v] = min(a[u][v], w);
	}
	f[1] = 0;
	for (ll i = 1; i <= n; ++i)
		for (ll j = 1; j < i; ++j)
			if (f[j] < 1 << 30 && a[j][i] < 1 << 30)
				f[i] = min(f[i], f[j] + a[j][i]);
	printf("%lld\n", f[n]);
	return 0;
}

第三题:

给定一个长度为n的数组a1,a2...,an,问其中的最长上升子序列的长度。也就是说,我们要找到最大的m以及数组pm,满足1<=p1<p2<...<pm<=n并且a(p1)<a(p2)<...<a(pm)

最优子结构:为了算出a1,a2...ai中以i这个位置结尾(i这个位置必须取)的最长上升子序列的长度,对于所有小于i的位置j,我们可以先计算出a1,a2,...,aj以j这个位置结尾的最长上升子序列的长度。如果aj<ai,则以j这个位置结尾的上升子序列加上ai,构成以i这个位置结尾的上升子序列。

我们只关注以i这个位置结尾的最长上升子序列,不关心子序列具体长啥样。

#include <bits/stdc++.h>
using namespace std;
int n, a[1001], f[1001];
int main() {
	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 i = 1; i <= n; ++i)
		for (int j = 1; j < i; ++j)
			if (a[j] < a[i])
				f[i] = max(f[i], f[j] + 1);
	int ans = 0;
	for (int i = 1; i <= n; ++i)	
		ans = max(ans, f[i]);
	printf("%d\n", ans); 
	return 0;
}

第四题:

给定一个长度为n(1<=n<=1000)的数组,a1,a2...an以及一个长度为m(1<=m<=1000)的数组b1,b2,...bm问a和b的最长公共子序列的长度。也就是说,我们要找到最大的k以及数组p1,p2...pk数组l1,l2,...lk<=m并且对于所有i,a(pi) = b(li)。

如果ai = bj,考虑取前i-1个元素和b中的j-1情况下的最优解,在例子中为

如果ai != bj,考虑a中前i-1个元素和b中的前j个元素情况下的最优解,即ai不取。考虑a中的前i个元素和b中的前j-1个元素情况下的最优解,即bj不取。

最优子结构:为了算出ai,a2,...ai和b1,b2...bj最长公共子序列的长度,我们需要知道a1,a2...a(i-1)和b1,b2,...b(j-1)和a1,a2,...,a(i-1)和b1,b2,...b(j-1)最长公共子序列的长度。a1,a2...a(i-1)和b1,b2,...bj)、a1,a2,...,ai和b1,b2,...b(j-1)的公共子序列都是ai,a2,...ai和b1,b2...bj公共子序列、如果ai==bj,a1,a2...a(i-1)和b1,b2,...b(j-1)的公共子序列加上ai,也是ai,a2,...ai和b1,b2...bj的公共子序列。

无后效性:我们只关心ai,a2,...ai和b1,b2...bj的公共子序列长度,不关心公共子序列具体长啥样。

状态:f[i][j]表示a1,a2...ai和b1,b2...bj的最长公共子序列的长度

转移:f[i][j]可以从3个地方转移过来

f[i][j] = max(f[i][j], f[i - 1][j])

f[i][j] = max(f[i][j], f[i][j - 1])

如果ai == bj,f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1)

AC代码:

#include <bits/stdc++.h>
using namespace std;
int n, m, a[1001], b[1001], f[1001][1001];
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i)
		scanf("%d", &a[i]);
	for (int j = 1; j <= m; ++j)
		scanf("%d", &b[j]);
	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);
		}
	printf("%d\n", f[n][m]);
	return 0;
}

第五题:

给定一个长度为n的数组,a1,a2...,an.问其中最长回文子串的长度

定义子串al,a(l + 1),...,ar为回文子串,当且仅当这个子串正着看和反着看是一样的,既有al = ar, a(l + 1) = a(r - 1)...。

首先定义 dp[i][j],用来存储输入字串s索引为i和索引为j之间的子串s[i,j]是否为回文串

1. dp[i][j]=true,则s[i,j]是回文串
2. dp[i][j]=false,则s[i,j]不是回文串

所以我们要去遍历l的长度,来获取最大值。

最优子结构:如果我们想知道 s[i,j] 的情况,不需要调用判断回文串的函数了,只需要知道 s[i+1,j-1] 的情况就可以了。问题变为 s[i] == s[j] && dp[i+1,j-1]

无后效性:我们只关心dp[i + 1][j - 1]是不是回文子串,不关心他是从具体是那些字母组成的串。

AC代码:

#include <bits/stdc++.h>
using namespace std;
int n, ans, a[1001], dp[1001][1001];
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i)
		scanf("%d", &a[i]);
	for (int i = 1; i <= n; ++i) {
		dp[i][i] = 1;
		if (i < n && a[i] == a[i + 1]) 
			dp[i][i + 1] = 1;
	}
	for (int l = 3; l <= n; ++l)
		for (int i = 1; i + l - 1 <= n; ++i) {
			int j = i + l - 1;
			if (a[i] == a[j] && dp[i + 1][j - 1]) {
				dp[i][j] = 1;
				ans = l;
			}
		}
	printf("%d\n", ans);
	return 0;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

雨后有晴天

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

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

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

打赏作者

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

抵扣说明:

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

余额充值