动态规划
三角形最小路径和
描述
给定一个正三角形数组,自顶到底分别有 1,2,3,4,5...,n 个元素,找出自顶向下的最小路径和。
每一步只能移动到下一行的相邻节点上,相邻节点指下行种下标与之相同或下标加一的两个节点。
数据范围:三角形数组行数满足 1≤n≤200 1≤n≤200 ,数组中的值都满足 ∣val∣≤104 ∣val∣≤104
例如当输入[[2],[3,4],[6,5,7],[4,1,8,3]]时,对应的输出为11,
所选的路径如下图所示:
思路
从倒数第二行开始,f【i】【j】的值 = 自己的值 +( f【i+1】【j】与f【i+1】【j+1】)值的最小值
代码
#include <vector>
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param triangle int整型vector<vector<>>
* @return int整型
*/
int minTrace(vector<vector<int> >& triangle) {
// write code here
int n = triangle.size();
for(int i = n -2;i>=0;i--){
for(int j = 0;j<triangle[i].size();j++){
triangle[i][j] += min(triangle[i+1][j],triangle[i+1][j+1]);
}
}
return triangle[0][0];
}
};
打劫
思路
设 dp[i]dp[i]dp[i] 表示到达第 iii 间房子时能够偷窃到的最高金额,则有递推公式:
dp[i]=max(dp[i−1],dp[i−2]+nums[i])dp[i] = \max(dp[i-1], dp[i-2] + nums[i])
dp[i]=max(dp[i−1],dp[i−2]+nums[i])
其中:
- dp[i−1]dp[i-1]dp[i−1] 表示不偷第 iii 间房子,继承之前的最高金额;
- dp[i−2]+nums[i]dp[i-2] + nums[i]dp[i−2]+nums[i] 表示偷第 iii 间房子,则不能偷第 i−1i-1i−1 间,故加上 dp[i−2]dp[i-2]dp[i−2]。
边界条件为:
- dp[0]=nums[0]dp[0] = nums[0]dp[0]=nums[0]
- dp[1]=max(nums[0],nums[1])dp[1] = \max(nums[0], nums[1])dp[1]=max(nums[0],nums[1])
最终答案为 dp[n−1]dp[n-1]dp[n−1],其中 nnn 为房屋数。
代码
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 0) return 0;
if(n == 1) return nums[0];
vector<int> dp(n,0);
dp[0] = nums[0];
dp[1] = max(nums[0],nums[1]);
for(int i =2;i<n;i++){
dp[i] =max( dp[i - 1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
};
单词拆分
思路
- 定义 dp[i] 表示字符串 s 的前 i 个字符(即 s[0...i-1])是否能由字典中的单词拼接而成。
- 初始状态 dp[0] = true,表示空字符串可以被拆分。
- 对于每个位置 i,从 0 到 i-1 检查,如果 dp[j] 为 true 且 s[j...i-1] 在字典中,则 dp[i] 为 true。
- 最终返回 dp[s.size()]。
代码
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
// 使用哈希集合提高查找效率
unordered_set<string> dict(wordDict.begin(), wordDict.end());
int n = s.size();
vector<bool> dp(n+1, false);
dp[0] = true; // 空字符串可以拆分
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
// 如果 s[0...j-1] 可拆分,并且 s[j...i-1] 在字典中,则 s[0...i-1] 可拆分
if (dp[j] && dict.find(s.substr(j, i - j)) != dict.end()) {
dp[i] = true;
break; // 一旦成立,无需继续检查
}
}
}
return dp[n];
}
};
零钱兑换
思路
dp【i】表示i块需要的最少硬币数量
代码
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
// 初始化 dp 数组,初始值为 amount+1,amount+1 是一个不可能达到的硬币数
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
// 遍历所有金额,从 1 到 amount
for (int i = 1; i <= amount; i++) {
// 对于每个硬币面额 coin
for (int coin : coins) {
if (i >= coin) {
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
}
// 如果 dp[amount] 没有被更新,则说明无法凑成 amount,返回 -1,否则返回 dp[amount]
return dp[amount] > amount ? -1 : dp[amount];
}
};
最长严格上升子序列
思路
dp【i】表示以i结尾的严格上升子序列的最大长度
dp【i】=max(dp【j】+1)
代码
#include <iostream>
using namespace std;
int main() {
int n;
cin>>n;
if(n == 0){
cout<<0;
return 0;
}
int arr[n];
int dp[n];
for(int i =0;i<n;i++){
cin>>arr[i];
dp[i]=1;
}
int ans= 1;
for(int i = 0;i<n;i++){
for(int j = 0;j<i;j++){
if(arr[j]<arr[i]){
dp[i] = max(dp[j] + 1,dp[i]);
}
}
ans = max(ans, dp[i]);
}
cout<<ans;
}
// 64 位输出请用 printf("%lld")
最长公共子序列
思路
- 定义 dp 数组大小为 (n+1)×(m+1),其中 dp[i][j] 表示 s1[0..i-1] 与 s2[0..j-1] 的 LCS 长度。
- 边界:当 i 或 j 为 0 时,dp[i][j] = 0(空字符串与任意字符串的 LCS 长度均为 0)。
- 状态转移:
- 如果 s1[i-1] == s2[j-1],则 dp[i][j] = dp[i-1][j-1] + 1;
- 否则 dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
- 最终 dp[n][m] 即为答案。
代码
#include <cstring>
#include <iostream>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
string s1, s2;
cin >> s1 >> s2;
// 构造 (n+1) x (m+1) 的二维dp数组,初始值为0
int dp[n+1][m+1];
memset(dp,0, sizeof(dp));
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++){
if (s1[i - 1] == s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
cout << dp[n][m] << "\n";
return 0;
}
// 64 位输出请用 printf("%lld")
装箱问题
代码
#include <iostream>
#include <vector>
using namespace std;
int main(){
int V, n;
cin >> V >> n;
vector<int> items(n);
for (int i = 0; i < n; i++){
cin >> items[i];
}
// 定义 dp 数组,dp[j] 表示是否可以恰好填满体积 j
vector<bool> dp(V + 1, false);
dp[0] = true; // 体积 0 总是可以达到
// 对每个物品进行动态规划更新
for (int i = 0; i < n; i++){
for (int j = V; j >= items[i]; j--){
if(dp[j - items[i]]){
dp[j] = true;
}
}
}
// 找到能达到的最大体积
int best = 0;
for (int j = V; j >= 0; j--){
if(dp[j]){
best = j;
break;
}
}
// 箱子的剩余空间 = 总容量 V - 装入物品的体积 best
cout << V - best ;
return 0;
}
01背包
思路
动态规划思路
我们定义二维数组 dp,其中
dp[i][j]dp[i][j]
dp[i][j]
表示从前 i 个物品中选择若干个,使得所占体积恰好为 j 时,所能获得的最大价值。
如果无法凑出 j 的体积,则令 dp[i][j] 为负无穷(这里可以用一个足够小的数表示)。
初始化:
- dp[0][0] = 0
- 对于 j > 0,dp[0][j] = -∞\infty∞(这里可以用 -1000000000 作为负无穷,因为最大价值不会超过 1000*1000=1e6)。
状态转移:
对于第 i 个物品(i 从 1 到 n),体积为 v[i]、价值为 w[i],
-
如果不选第 i 个物品:dp[i][j] = dp[i-1][j]
-
如果选第 i 个物品(前提 j ≥ v[i]):
dp[i][j] = dp[i-1][j - v[i]] + w[i]
因此:
dp[i][j]=max(dp[i−1][j], dp[i−1][j−v[i]]+w[i])(j≥v[i])dp[i][j] = \max(dp[i-1][j],, dp[i-1][j-v[i]] + w[i]) \quad (j \ge v[i])
dp[i][j]=max(dp[i−1][j],dp[i−1][j−v[i]]+w[i])(j≥v[i])
若 j < v[i],只能不选。
答案计算:
-
第一问的答案为:0≤j≤Vmaxdp[n][j]
max0≤j≤Vdp[n][j]\max_{0 \le j \le V} dp[n][j]
-
第二问的答案为 dp[n][V];若 dp[n][V] 小于 0(表示没有方案恰好装满),则输出 0。
代码
#include <climits>
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;
int main() {
int n;
int v;
cin>>n>>v;
int volume[n+1];
int w[n+1];
for(int i = 1;i<=n;i++){
cin>>volume[i]>>w[i];
}
vector<vector<int>> dp(n+1,vector<int>(v+1,INT_MIN));
dp[0][0] = 0;
for(int i = 1;i<=n;i++){
for(int j = 0;j<=v;j++){
dp[i][j] = dp[i-1][j];
if(j>=volume[i]&&dp[i-1][j-volume[i]]!=INT_MIN){
dp[i][j] = max(dp[i][j],dp[i-1][j-volume[i]]+w[i]);
}
}
}
int ans1 = 0;
for (int j = 0; j <= v; j++){
ans1 = max(ans1, dp[n][j]);
}
int ans2 = dp[n][v];
if (ans2 < 0) ans2 = 0;
cout<<ans1<<endl<<ans2;
}
// 64 位输出请用 printf("%lld")
购物单问题(01背包变体)
思路:每个主件可以单独买,也可以带 1~2 个附件一起买。
把一件主件及其附件视为一组,对每组枚举“选哪几件”状态(0、主件、主件+附件1、主件+附件2、主件+附件1+附件2),在组与组之间做 0/1 背包。
代码
#include <iostream>
#include <map>
#include <vector>
using namespace std;
int main() {
int N, m;
cin >> N >> m;
// 由于价格都是 10 的整数倍,先除以 10 以压缩状态空间
N /= 10;
// prices[i][0] 存第 i 件主件的“折算后”价格
// priceMultiplyPriority[i][0] 存第 i 件主件的 价格×重要度
// prices[i][1..2]、priceMultiplyPriority[i][1..2] 对应它最多两个附件
vector<vector<int>> prices(61, vector<int>(3, 0));
vector<vector<int>> priceMultiplyPriority(61, vector<int>(3, 0));
for (int i = 1; i <= m; ++i) {
int a, b, c;
cin >> a >> b >> c;
a /= 10; // 价格也同样压缩
b *= a; // 直接存“价值 = 价格 × 重要度”
if (c == 0) {
// 主件,存在 [i][0]
prices[i][0] = a;
priceMultiplyPriority[i][0] = b;
} else {
// 附件挂到编号为 c 的主件下
// 如果第 c 件主件还没放附件,就放到 [c][1],否则放到 [c][2]
if (prices[c][1] == 0) {
prices[c][1] = a;
priceMultiplyPriority[c][1] = b;
} else {
prices[c][2] = a;
priceMultiplyPriority[c][2] = b;
}
}
}
// dp[i][j] 表示:只考虑前 i 件“主件组”,在预算 j(已除 10)下的最大价值
vector<vector<int>> dp(m + 1, vector<int>(N + 1, 0));
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= N; ++j) {
// 主件和附件的“折算后”价格和价值
int a = prices[i][0], b = priceMultiplyPriority[i][0];
int c = prices[i][1], d = priceMultiplyPriority[i][1];
int e = prices[i][2], f = priceMultiplyPriority[i][2];
// 1) 不买这组:dp[i][j] = dp[i-1][j]
dp[i][j] = dp[i - 1][j];
// 2) 只买主件
if (j >= a)
dp[i][j] = max(dp[i][j], dp[i - 1][j - a] + b);
// 3) 主件 + 附件1
if (c > 0 && j >= a + c)
dp[i][j] = max(dp[i][j], dp[i - 1][j - a - c] + b + d);
// 4) 主件 + 附件2
if (e > 0 && j >= a + e)
dp[i][j] = max(dp[i][j], dp[i - 1][j - a - e] + b + f);
// 5) 主件 + 附件1 + 附件2
if (c > 0 && e > 0 && j >= a + c + e)
dp[i][j] = max(dp[i][j], dp[i - 1][j - a - c - e] + b + d + f);
}
}
// 乘回 10,恢复真实价格单位
cout << dp[m][N] * 10 << endl;
}
// 64 位输出请用 printf("%lld")
跳台阶拓展
一次可以跳1到n阶台阶
发现跳法数 = 2^(n-1),直接输出即可
最小花费爬楼梯
可以从第 0 阶或第 1 阶出发,且出发时不需要花费任何代价。
思路
dp[i] = min(dp[i-1] + cost[i-1],dp[i-2]+cost[i-2]);dp代表爬到i层的最小花费
代码
#include <iostream>
using namespace std;
int main() {
int n;
cin>>n;
int cost[n];
for(int i =0;i<n;i++){
cin>>cost[i];
}
int dp[n+1];
dp[0] =0;
dp[1] =0;
for(int i = 2;i<=n;i++){
dp[i] = min(dp[i-1] + cost[i-1],dp[i-2]+cost[i-2]);
}
cout<<dp[n];
}
// 64 位输出请用 printf("%lld")
矩阵的最小路径和
代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> a(n, vector<int>(m));
for(int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
cin >> a[i][j];
}
}
// 定义 dp 数组,dp[i][j] 表示从 (0,0) 到 (i,j) 的最小路径和
vector<vector<int>> dp(n, vector<int>(m, 0));
// 初始条件
dp[0][0] = a[0][0];
// 第一行
for (int j = 1; j < m; j++){
dp[0][j] = dp[0][j-1] + a[0][j];
}
// 第一列
for (int i = 1; i < n; i++){
dp[i][0] = dp[i-1][0] + a[i][0];
}
// 状态转移
for (int i = 1; i < n; i++){
for (int j = 1; j < m; j++){
dp[i][j] = a[i][j] + min(dp[i-1][j], dp[i][j-1]);
}
}
cout << dp[n-1][m-1] << "\n";
return 0;
}
拦截导弹
描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于1000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
数据范围:导弹个数 n 满足 1≤n≤1000 1≤n≤1000 ,导弹的高度 m 满足 1≤m≤1000 1≤m≤1000
输入描述:
第一行输入一个正整数 n ,表示导弹的个数
第二行输入 n 个正整数,表示导弹的高度
输出描述:
输出一套拦截系统最多拦截多少导弹和最少要配备多少套导弹拦截系统两个正整数
思路
最少的下降序列等于最长的上升序列长度
代码
#include <iostream>
#include <ostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> height(n);
for (int i = 0; i < n; i++) {
cin >> height[i];
}
vector<int> dp(n, 1);
vector<int> dp2(n,1); // 最少的下降序列等于最长的上升序列长度
int ans = 1;
int ans2 = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (height[j] >= height[i]) {
dp[i] = max(dp[j] + 1, dp[i]);
}else {
dp2[i] = max(dp2[j]+1,dp2[i]);
}
}
ans = max(ans, dp[i]);
ans2 = max(ans2,dp2[i]);
}
cout << ans << endl <<ans2;
}
// 64 位输出请用 printf("%lld")
合唱队形
描述
N位同学站成一排,音乐老师要请其中的 (N-K) 位同学出列,使得剩下的K位同学排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为 1,2…,K,他们的身高分别为 T1,T2,…,TK, 则他们的身高满足 t1<t2...
你的任务是,已知所有 n 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
数据范围: 1≤n≤1000 1≤n≤1000 ,身高满足 130≤ti≤230 130≤ti≤230
输入描述:
第一行输入一个正整数 n 表示同学的总数。
第二行有 n 个整数,用空格分隔,第 i 个整数 ti 是第 i 位同学的身高(厘米)。
输出描述:
输出仅有一个整数,即最少需要几个同学出列
思路
代码
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> height(n);
vector<int> lis(n,1);
vector<int> lds(n,1);
for(int i =0;i<n;i++){
cin>>height[i];
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (height[i]>height[j]) {
lis[i] = max(lis[i],lis[j] + 1);
}
}
}
for (int i = n-1; i >=0; i--) {
for (int j = n-1; j>i; j--) {
if (height[j]<height[i]) {
lds[i] = max(lds[i],lds[j] + 1);
}
}
}
int maxLen = 0;
for(int i =0;i<n;i++){
maxLen = max(maxLen, lis[i] + lds[i] - 1);
}
cout<<(n - maxLen);
}
// 64 位输出请用 printf("%lld")
滑雪
思路
深搜+动态规划 dp i j表示i j出发的最长降序序列
代码
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<vector<int>> grid;
vector<vector<int>> dp;
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
int dfs(int i, int j) {
if (dp[i][j] != 0) {
return dp[i][j];
}
int best = 1;
for (int k = 0; k < 4; i++) {
int x = i + dx[k], y = j + dy[k];
if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] < grid[i][j]) {
best = max(1 + dfs(x, y), best);
}
}
dp[i][j] = best;
return dp[i][j];
}
int main() {
cin >> n >> m;
grid.resize(n, vector<int>(m));
dp.resize(n, vector<int>(m, 0));
int ans = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> grid[i][j];
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; i++) {
ans = max(ans, dfs(i, j));
}
}
cout<<ans;
}
// 64 位输出请用 printf("%lld")
最长回文子序列
思路
- 令 dp[i][j] 表示字符串 s 从下标 i 到 j 范围内的最长回文子序列的长度。
- 状态转移:
- 如果 s[i] == s[j],那么 dp[i][j] = dp[i+1][j-1] + 2;
- 否则 dp[i][j] = max(dp[i+1][j], dp[i][j-1])。
- 边界条件:当 i == j 时,dp[i][i] = 1(单个字符就是回文);当 i > j 时,dp[i][j] = 0。
- 最终答案为 dp[0][n-1]。
- 从len =2开始,遍历到len = n结束。
代码
#include <iostream>
#include <string>
#include<vector>
using namespace std;
int main() {
string str;
cin>>str;
if(str.size() == 1){
cout<<1;
return 0;
}
int n = str.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = 0;i<n;i++){
dp[i][i] = 1;
}
for (int len = 2; len <= n; len++){
for (int i = 0; i <= n - len; i++){
int j = i + len - 1;
if (str[i] == str[j]){
dp[i][j] = (len == 2 ? 2 : dp[i+1][j-1] + 2);
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
}
}
cout << dp[0][n-1];
}
// 64 位输出请用 printf("%lld")
跳跃游戏二
思路
dp i表示到i能达到的最大积分。对每个i遍历i+1到n-1的位置,dp j = max(dp j,dp i + nums【i】)
代码
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n ;
cin>>n;
if(n == 0){
cout << -1 << "\n";
return 0;
}
vector<int> nums(n);
vector<int> dp(n,-1);
for(int i =0;i<n;i++){
cin>>nums[i];
}
dp[0] = nums[0];
for(int i =0;i<n;i++){
if(dp[i] == -1) continue;
int end = min(n-1,i+nums[i]);
for(int j = i+1;j<=end;j++){
dp[j] = max(dp[j],dp[i]+nums[j]);
}
}
cout<<dp[n-1];
}
// 64 位输出请用 printf("%lld")
跳跃游戏三
输出跳刀最后的最少步数
dp i 表示到达i的最少步数
代码
#include <climits>
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin>>n;
if(n == 0){
cout<<-1;
return 0;
}
const int INF = 1e9;
vector<int> nums(n);
vector<int> dp(n,INF);
for(int i = 0;i<n;i++){
cin>>nums[i];
}
dp[0] = 0;
for(int i =0;i<n;i++){
int end = min(n-1,nums[i]+i);
for(int j = i+1;j<=end;j++){
dp[j] = min(dp[j],dp[i] + 1);
}
}
int ans = dp[n-1];
if(ans==INF)
ans= -1;
cout<<ans;
}
// 64 位输出请用 printf("%lld")