一、导入
问题:楼梯一共有 n 阶,上楼可以一步上一阶,也可以一步上二阶。请求出走到第 n 阶共有多少种不同的走法。
方法一:
搜索,我们写一个dfs,依次枚举每一步向上走多少台阶,最后统计有多少可行的方案。
方法二:排列组合
我们先把表示方法简化:
只用1和2的序列表示从下到上每一步分别走了多少台阶。
2 2 2 2 2表示一共走了五步,每次走两级台阶,这是一种有效的走法
1 1 1 1 2 2一共走了7步,前四步走一级台阶后三步走二级台阶,这也是有效的走法
接着枚举一共几步走了2级台阶,就能算出有几步走了1级台阶,最后可以用组合数学算。
10 = 10 * 1(10个1,共 = 1种)
+ 8 * 1 + 2 * 1(8个1,1个2,共 = 9 种)
+ 6 * 1 + 2 * 2 (6个1,2个2,共 = 28种)
+ 4 * 1 + 3 * 2(4个1,3个2,共 = 35种)
+ 2 * 1 + 4 * 2 (2个1, 4个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;
}