组合和排列是经典的递归回溯问题,并且可以利用剪枝的技巧对其进行优化。
方法1:非剪枝的递归回溯方法
组合不考虑顺序,从给定的N个数字中选定M个进行组合,输出组合的数字。
1. 组合需要选数,排列不需要
2. 那么可以考虑成当前位置的数选还是不选
3. 选,则数加入组合,不选则直接跳过
4. 当组合内数量满M时,输出组合并return
5. 当所有的数都经过选与不选的抉择后,也进行return
6. 回溯之后需要状态恢复,进入另一个状态分支——不选当前的数
考虑清楚上面几个问题,则很容易写出以下代码:
class Solution {
public:
vector<vector<int>> ans;
vector<int> temp;
void dfs(int cur, int n, int k)
{
if(temp.size()==k)//选满k个数进行回溯
{
ans.push_back(temp);
return;
}
if(cur==n+1)//当前指针越界也回溯
{
return;
}
temp.push_back(cur);//选择当前的数
dfs(cur+1, n, k);
temp.pop_back();//状态恢复
dfs(cur+1, n, k);//不选择当前的数
}
vector<vector<int>> combine(int n, int k) {
dfs(1, n, k);
return ans;
}
};
方法2:剪枝优化
1. 考虑到存在大量不必要的判断,可以对当前状态进行判断,提前剪枝
2. 剪枝条件:当后续的数字全选依然无法满足组合中存在M个数时,该状态可以提前剪枝
3. 可以证明,该剪枝条件中包含了指针越界的情况
则代码可以优化为:
class Solution {
public:
vector<vector<int>> ans;
vector<int> temp;
void dfs(int cur, int n, int k)
{
//提前剪枝,当后续的数字全选也不够K个时,直接返回
if(temp.size()+(n-cur+1)<k)
{
return;
}
if(temp.size()==k)
{
ans.push_back(temp);
return;
}
temp.push_back(cur);
dfs(cur+1, n, k);
temp.pop_back();
dfs(cur+1, n, k);
}
vector<vector<int>> combine(int n, int k) {
dfs(1, n, k);
return ans;
}
};
方法一:next_permutation
第一种方法属于利用函数作弊的方法,next_permutation可以返回当前排列的下一个字典序排列,由于刚开始给出的不一定是从小到大的最小字典序,所以需要利用sort进行排序后,调用next_permutation进行输出
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>>ans;
sort(nums.begin(), nums.end());
do//注意这里需要使用do-while结构将最开始的排列也加入
{
ans.push_back(nums);
}while(next_permutation(nums.begin(), nums.end()));
return ans;
}
};
方法二:递归+回溯
1. 全排列只考虑数的位置
2. 如何获取全部的顺序?利用递归+循环+回溯实现
3. 建立一个visit数组记录所有位置的元素的访问情况
4. 每次需要选择元素时,将没有访问过的元素按从小到大的顺序加入,然后visit置为true
5. 当元素符合N个时表明当前排列完成,则进行输出,并return
6. 回溯是将该元素的visit对应位置修改为false,并从排列中删除
例子演示:
1234的全排列
首先第一个排列没有回溯,输出1234
回溯后4的visit变为false,此时排列中为123
由于4的循环到头所以循环退出,到3的循环中
此时对3进行回溯,visit置为false,排列中剩余12
3的循环没有走完,所以下一个位置4未被访问,则4进入排列中,visit变为true,此时排列为124
新的递归中对没有标记的元素遍历时,只有3没被访问,则入排列,visit变为true,此时排列1243
元素满足排列数量,则输出,进行新一轮的回溯
……
class Solution {
public:
vector<int>temp;
vector<bool>visit;
vector<vector<int>>ans;
void dfs(vector<int>&nums, int n)
{
if(temp.size()==n)
{
ans.push_back(temp);
return;
}
for(int i=0;i<n;i++)
{
if(!visit[i])//寻找未在当前排列中的数字
{
temp.push_back(nums[i]);
visit[i] = true;
dfs(nums, n);//递归调用
visit[i] = false;//回溯
temp.pop_back();
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
visit.resize(n, false);//reshape
dfs(nums, n);
return ans;
}
};
1. 对于大写字母:两种选择——不变或者变为小写字母
2. 对于小写字母:两种选择——不变或者变为大写字母
3. 对于数字:一种选择——不变
排列的多样主要通过大小写的变化而得到
所以需要写分条件的递归和回溯
4. 当前字符为大小写字母时,需要进行递归和回溯——类似组合中的选与不选的回溯
5. 当前字符为数字时,直接加入即可
6. 当字符的个数符合排列的数量时,输出并返回
class Solution {
public:
vector <string> ans;
void huisu(string s, int n, int cur)
{
if(cur==n+1)
{
ans.push_back(s);
return;
}
if(s[cur]>='a'&&s[cur]<='z')//分情况对于当前字符进行递归和回溯
{
huisu(s, n, cur+1);
s[cur] = s[cur]-32;//转换为大写
huisu(s, n, cur+1);
}
else if(s[cur]>='A'&&s[cur]<='Z')
{
huisu(s, n, cur+1);
s[cur] = s[cur]+32;//转换为小写
huisu(s, n, cur+1);
}
else
{
huisu(s, n, cur+1);//不变直接递归
}
}
vector<string> letterCasePermutation(string s) {
int n = s.size();
huisu(s, n, 0);
return ans;
}
};
动态规划把握好三个方面:
- 大问题转换为小问题——每一个小问题的解是否容易求得?是否存在关系?(问题转换)
- 动态规划的初始条件——最小规模下的问题的解?一般在DP之前进行定义(最小规模)
- 动态规划的状态转移方程——由小问题之间的递推关系求得(状态转换)
规模更小的问题:
到第一层台阶有几种方法?
到第二层台阶有几种方法?
……
到第i-1层台阶有几种方法?
可以想象:
当前站在第一层台阶时,一次跨两步就可以到第三层;
当前站在第二层台阶时,一次跨一步就可以到第三层;
那么从底层到第三层的方法就应该是到第一层台阶的方法+第二层台阶的方法
从前面的状态推得当前状态就可以利用DP方法实现
——一个疑问,从第一层台阶到第三层还可以两次跨一步,为什么不算上呢?
——其实这种方法在上到第二层台阶时已经计算过一次,所以不需要重复计算
所以递推公式可以写成如下形式:
dp[i] = dp[i-1]+d[i-2]
是的就是大家非常熟悉的斐波那契数列
class Solution {
public:
int climbStairs(int n) {
if(n==1)
return 1;
if(n==2)
return 2;
int a = 1;
int b = 2;
int ans;
while((n--)-2)
{
ans = a + b;
a = b;
b = ans;
}
return ans;
}
};
题目的大概意思是求得最大的不连续数字之和。
最开始的想法是不连续且最大,那不就是奇数和偶数位置相加比较不就行了——当然不行
比如例子:4,1,1,4,最大的不连续数字之和应该是8,而不是奇偶位置。
1.规模缩放:
只有一个屋子时偷窃的最高金额?
只有两个屋子时偷窃的最高金额?
……
只有n个屋子时偷窃的最高金额?
2.初始状态:
只有一个屋子时,则偷窃的最高金额为该屋子中的金额,用dp数组表示有i个屋子时偷窃的最高金额,则
dp[0] = nums[0]
3.状态转移方程:
由于条件规定无法偷窃相邻的房间,所以计算含i个屋子时的偷窃的最高金额是的状态转移方程:
dp[i] = max{dp[i-2]+nums[i], dp[i-1]}——用以表示当前屋子是否偷取
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n==1)
return nums[0];
if(n==2)
return max(nums[0], nums[1]);
vector<int> dp(n);
dp[0] = nums[0];
dp[1] = max(nums[1], dp[0]);//状态初始化
for(int i=2;i<n;i++)
{
dp[i] = max(nums[i]+dp[i-2], dp[i-1]);
}
return dp[n-1];
}
};
本题可以利用滚动数组对空间进行优化,仅保存第i-1和第i-2个dp结果即可
二维DP,作为背包问题的一个入门热身题目,也是非常经典的DP题目之一
给定三角形数据的自顶向下的最短路径,同样也是先计算i-1层的最短路径,在此基础上计算第i层的最短路径,规定只能从相邻节点往下,也就规定了状态转移方程的写法
初始条件:第一层的最短路径就是其本身dp[0][0] = triangle[0][0]
状态转移方程:
d[i][j] = max{triangle[i][j]+d[i-1][j], triangle[i][j]+d[i-1][j-1]}
从上一层相邻的两个元素往下计算
这里需要注意边界条件,当元素处于该行的开头或结尾时,只存在一个上层的相邻元素
if(j==0)
d[i][j] = triangle[i][j]+d[i-1][j]
if(j==col-1)
d[i][j] = triangle[i][j]+d[i-1][j-1]
空间优化——滚动数组:可以知道当前状态仅和上一层状态有关,则可以利用两个长度为n的一维数据存储新旧结果,在每一层结果计算之后,将结果进行更新即可
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int n = triangle.size();
vector<int> dp(n, 0);//存储第i层到当前元素的最短路径
int ans = INT_MAX;
if(n==1)
{
return triangle[0][0];
}
dp[0] = triangle[0][0];//状态初始化
for(int i=1;i<n;i++)
{
vector<int> temp(n, 0);//临时存储计算结果
for(int j=0;j<triangle[i].size();j++)
{
if(j!=0&&j!=triangle[i].size()-1)
{
temp[j] = min(dp[j]+triangle[i][j], dp[j-1]+triangle[i][j]);
//cout<
}
else if(j==0)
{
temp[j] = dp[j]+triangle[i][j];
//cout<
}
else
{
temp[j] = dp[j-1]+triangle[i][j];
//cout << dp[j]<
}
}
for(int t=0;t<temp.size();t++)//将结果进行复制
dp[t] = temp[t];
}
for(auto &it:dp)//从最后一层的结果中挑选最小的路径长度返回
{
if(it<ans)
ans = it;
}
return ans;
}
};