文章目录
前言
一篇一篇来好麻烦,反正是自己看,全部写一起了
15题往后的题解结构和15题相同,后面的在目录上就不体现了
15_爬楼梯(进阶版)
15.1_题目
题目链接:爬楼梯(进阶版)
15.2_题解
15.2.1_题目分析
多少种方法->排列组合问题
题目给的案例->排列问题(不同顺序的相同组成也算作一种不同的方案)
步数可选,达成给定总和->背包问题
每个步数可以重复选择->完全背包问题
题目给定一个m(每次至多爬m层台阶)->从1到m均为可重复选择的“物品”
15.2.2_动态规划
15.2.2.1_dp分析五部曲(因为懒,④和⑤就没写过)
①dp数组含义:
dp[j]表示爬到第j层台阶的方案数
②递推公式:
每次至多爬m层台阶—本次爬多少层台阶相当于物品价值,又因为刚好用i遍历物品价值,递推公式中直接用i进行计算
if(j>=i ) dp[j]+=dp[j-i )+i
③dp数组初始化
初始化dp[0]=1—初始条件下的dp[0]为没有步数可以选择时,爬上第0层台阶的方案数,此时不作选择也算一种方案,所以初始化为1
④遍历顺序
因为是排列问题:所以外循环为背包容量,内循环为物品价值
此题中外循环为目标阶数,内循环为本次爬的台阶层数
⑤模拟推演
懒得推,后面这项就不写了
15.2.2.2_完整代码
int getCount(int n, int m) {
// 定义dp数组&全部初始化为0
// dp数组含义:dp[i]表示爬到第i层台阶的方案数
vector<int> dp(n+1,0);
// 对于第0层台阶,不爬也算为一种方案
dp[0] = 1;
// 利用递推公式遍历dp数组
// 排列问题:外循环为背包容量
for (int j = 1; j <= n; j++)
// 可爬阶数在1到m之间可选,所以i即为“价值”
for (int i = 1; i <= m; i++)
if (j >= i)
dp[j] += dp[j - i];
// 返回爬到第n层台阶的方案数
return dp[n];
}
16_322. 零钱兑换
16.1_题目
题目链接:322. 零钱兑换
16.2_题解
16.2.1_题目分析
返回需要硬币的最小个数—不考虑是排列还是组合
硬币可以重复选取—完全背包问题
16.2.2_动态规划
①dp数组含义
一般题目要求返回什么,含义就往什么上面去靠
本题dp[j]表示构成当前面额(j) 所需的最小硬币数量
②递推公式
dp[j]从一下情况中选较小值
1° 不考虑选取当前硬币(因为是一维滚动数组,所以在未改变dp[j]的值时,dp[j]即为没有考虑当前硬币的情况)时,已经能够凑成总面额的所需最小硬币数量
2° 当前需要凑成的总面额(j)减去当前硬币面额(coins[i])得到一个新的总面额,用新总面额的最小硬币数加一(dp[j-coins[i] ]+1)
③初始化
一维滚动数组初始化时对应的含义为
无可选择硬币时,从0到amount面额的最小组合硬币数量
题目要求不存在组合时返回-1
对于dp[0]:选择0个样本时,硬币面额之和等于总面额(0)
对于dp[1~amount]:没有硬币可以选择,所以初始化为INT_MAX因为在递推公式中需要选择较小值,而-1比正数小,所以要设置为一个不影响比较的值,而INT_MAX为最大整形数,很合适用作初始化
④遍历顺序
遍历顺序只要关心两个
第一个:内外层循环对应背包容量还是物品
1° 外循环为物品价值,内循环为背包容量时—组合问题(因为遍历物品的顺序被固定了)
2° 外循环为背包容量,内循环为物品价值时—排列问题(在内层遍历物品价值时,包含了所有的排序方式)
本题要求返回的是最小硬币数,所有无论是用组合还是排列取求最小值都可以
第二个:内层循环选择正序遍历还是逆序遍历dp数组(前提用的是一维滚动数组)
递推公式中dp值的求解会用到前面dp值(截取部分:dp[j]=…dp[j-coins[i] ]…)
1° 正序遍历对应完全背包问题:求dp值的时候使用的是本层循环更新过的。而不是上一层,相当于重复使用了当前物品(看dp[j]=…dp[j-coins[i] ]…,因为是正序,所以计算dp[j]时用到的dp[j-coins[i]也是使用本层物品计算出来的)
2° 逆序遍历对应01背包问题:倒序遍历不会重复使用本层物品
完整代码:
int coinChange(vector<int>& coins, int amount) {
// 定义dp数组&初始化
vector<int> dp(amount + 1, INT_MAX);
// 特殊项初始化---使用0个硬币恰好总面额为0
dp[0] = 0;
// 根据递推公式遍历dp数组
for (int i = 0; i < coins.size(); i++)
// 完全背包问题:内层循环正序遍历
for (int j = coins[i]; j < dp.size(); j++)
// 如果没有硬币可以组合成当前总面额减去当前硬币面额,则跳过(即不修改上一层的dp[j])
if (dp[j - coins[i]] != INT_MAX)
dp[j] = min(dp[j], dp[j - coins[i]] + 1);
// 如果目标总面额为被修改过,则返回-1
if (dp[amount] == INT_MAX)
return -1;
return dp[amount];
}
17_279. 完全平方数
17.1_题目
17.2_动态规划
分析
和题16几乎一样
差别在于物品价值不同
本题里不大于n的完全平方数即为物品价值
而且完全平方数中包含了1,所以不存在无解的情况
①dp数组含义
dp[j]表示j能最少能够用dp[j] 个完全平方数相加得到
②递推公式
dp[j]=min(dp[j],dp[j-i^2]+1)
在上一层和减去当前物品价值后的最小值加一中选择更小的
③初始化重要
dp[0]=0:对于目标数(0)最少选取0个完全平方数的和恰好为0
dp[1~n]:要初始化为INT_MAX,因为在遍历dp数组的时候,选取的min值,而初始化为0时代表没有组合,而递推公式中是在对存在组合的情况下进行赋值;
或者从i=1开始遍历,初始化i=1行,初始化值即为j/1=j
④遍历顺序
习惯用外循环为物品,内循环为背包容量
而内层循环则为正序遍历dp数组(完全背包问题)
完整代码
int numSquares(int n) {
// 定义dp数组&初始化为0
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
// 遍历dp数组
for (int i = 1; i * i <= n; i++)
for (int j = i * i; j < dp.size(); j++)
dp[j] = min(dp[j], dp[j - i * i] + 1);
return dp[n];
}
18_139. 单词拆分
18.1_题目
18.2_回溯法
18.2.1_未剪枝的回溯
①将回溯函数的返回值设置为bool类型
巧妙利用if进入回溯:
// 当前面查询到区间字符串属于字典单词时进入递归,否则跳过
// 当符合答案条件时,由另一个if语句返回true,同时此语句也就成立,返回true
if(dict.find(word)!=dict.end()&&backtracking(s,dict,i+1)
return true;
完整代码:
bool backtracking(string s,unordered_set<string>& wordDict,int startIndex) {
// 结束条件
if (startIndex >= s.length())
return true;
// 进入下层递归
for (int i = startIndex; i < s.length(); i++) {
string word = s.substr(startIndex, i - startIndex + 1);
// set中查找是否有此区间单词
if (wordDict.find(word) != wordDict.end() && backtracking(s,wordDict,i+1))
return true;
}
return false;
}
bool wordBreak(string s, vector<string>& wordDict) {
// 使用set哈希表,提高查找效率
unordered_set<string> dict(wordDict.begin(), wordDict.end());
// 回溯入口
return backtracking(s, dict, 0);
}
18.2.2_记忆化递归
记忆化递归记录了每个startIndex为起点的递归结果,当测试用例中含大量重复时较为有效
①记忆数组初始化为1,在进入以startIndex为起点的for循环前,判断当前startIndex是否计算过,如果计算过,直接返回计算结果
②计算结果能够被修改仅当返回false时,因为true直接返回就行(找到答案了)
完整代码:
bool backtracking(string s,unordered_set<string>& wordDict,vector<bool>& memory,int startIndex) {
// 结束条件
if (startIndex >= s.length())
return true;
// 查询当前起点是否计算过,若计算过直接返回记录的计算结果
if (!memory[startIndex])
return memory[startIndex];
// 进入下层递归
for (int i = startIndex; i < s.length(); i++) {
string word = s.substr(startIndex, i - startIndex + 1);
// set中查找是否有此区间单词
if (wordDict.find(word) != wordDict.end() && backtracking(s, wordDict, memory, i + 1))
return true;
}
memory[startIndex] = false;
return false;
}
bool wordBreak(string s, vector<string>& wordDict) {
// 使用set哈希表,提高查找效率
unordered_set<string> dict(wordDict.begin(), wordDict.end());
// 记录数组
vector<bool> memory(s.size(), 1);
// 回溯入口
return backtracking(s, dict,memory, 0);
}
18.3_动态规划重头戏
〇 题目分析
字符串s作为背包
单词字典作为可选物品
因为字典内单词可以重复选取—完全背包问题—内层循环正序遍历
字符串s中可能先出现的单词的字典序大于后出现的单词—应该选则排列循环方式
使用substr库函数截取当前区间的字符串,方便后面再set中查找
①dp数组含义
dp[i]表示长度为i的字符串能否由字典中的单词组合而来
②递推公式
// 截取当前区间字符串
string word=s.substr(j,j-i);
// 判断截取字符串是否存在于字典中&&此前的字符串是否能够由字典中的单词组成
if(set.find(word)!=set.end()&&dp[j]
dp[i]=true;
先判断当前区间的字符串是否为字典中的单词—set.find(word)!=set.end()
然后再判断当前字符串的长度减去当前单词的长度的字符串能否由字典中的单词组成—dp[j]==true
③初始化
dp[0]初始化为true:空集也为字典中的子集,能够选取空集组成空字符串,所以初始化为true
其余项字符串非空,皆不能由空集组成,所以初始化为false
④遍历顺序
因为s字符串中单词出现的顺序可能和字典中的顺序不同,所以应该选择排列
因为是可以重复选择完全背包问题,内层循环为正序遍历dp数组
完整代码:
bool wordBreak(string s, vector<string>& wordDict) {
// 建立set方便查找
unordered_set<string> set(wordDict.begin(), wordDict.end());
// 定义dp数组
vector<bool> dp(s.length() + 1,false);
dp[0] = true;
// 遍历dp数组
// 因为是排列,所以先遍历背包容量
// 这里的双重for循环是在
for (int i = 1; i < dp.size(); i++)
for (int j = 0; j < i; j++) {
string word = s.substr(j, i - j);
// 物品不用遍历,因为set可以直接查找
if (set.find(word) != set.end()&&dp[j])
// i为区间结束索引,用于上面if语句中的dp[j]
dp[i] = true;
}
return dp[s.length()];
}