class Solution {
public:
/*
dp[i][j]的含义为:子串[i:j]是回文子串
dp[i][j] = true, if s[i]==s[j] && dp[i+1][j-1]==true && i+1<=j-1
= true, if s[i]==s[j] && i+1==j
= false, else
dp[i][i] = 1
00 01 02 03 ... 0n
11 12 13 ... 1n
22 23 ... 2n
...
nn
从dp[i+1][j-1]到dp[i][j]是向右上方走,故dp的顺序是从下往上,从左边到右边
*/
string longestPalindrome(string s) {
int n = s.length() - 1;
vector<vector<bool>> dp(n + 1, vector<bool>(n + 1, false));
int re_i, re_j; // 记录最长回文子串左右两个指针
int re_len = 0; // 记录最长回文子串的长度
for(int i=n;i>=0;--i) {
for(int j=i;j<=n;++j) {
if(i == j) {
// 一个字符的子串
dp[i][j] = true;
}
else {
if(i == j - 1) {
// 两个字符的子串
if(s[i] == s[j]) {
dp[i][j] = true;
}
else {
dp[i][j] = false;
}
}
else {
// 大于两个字符的子串
if(dp[i+1][j-1] && s[i]==s[j]) {
dp[i][j] = true;
}
else {
dp[i][j] = false;
}
}
}
if(dp[i][j]) {
// [i:j]是回文子串,则尝试更新最长回文子串
if(j - i + 1 > re_len) {
re_i = i;
re_j = j;
re_len = j - i + 1;
}
}
}
}
return s.substr(re_i, re_len);
}
};
printf("%d", dp[i][j]); // 是一个非0整数
printf("%d", int(dp[i][j])); // 是一个非0整数
printf("%d", dp[i][j] == true); // 若dp[i][j] = true为1
printf("%d", 1 == true); // 是1
printf("%d", 0 == false); // 是1
printf("%d", true); // 是1
printf("%d", false); // 是0
dp[i][j]==1
的个数而不是统计j-i+1
的最大值;class Solution {
public:
/*
动态规划:
dp[i][j]:s[i:j]是否是回文子串
dp[i][j] = dp[i+1][j-1] && s[i]==s[j] if i+1<=j-1
= s[i]==s[j] otherwise
dp[i][i] = 1;
*/
int countSubstrings(string s) {
int n = s.length();
vector<vector<int>> dp(n, vector<int>(n, 0));
int re_sum = 0;
for(int i=n-1;i>=0;--i) {
for(int j=i;j<n;++j) {
if(i == j) {
dp[i][j] = 1;
++re_sum;
}
else {
if(i+1 <= j-1) {
if(dp[i+1][j-1] && s[i] == s[j]) {
dp[i][j] = 1;
++re_sum;
}
}
else {
if(s[i] == s[j]) {
dp[i][j] = 1;
++re_sum;
}
}
}
}
}
return re_sum;
}
};
思路:
针对以第i个字符结尾的最长子串长度进行动态规划;
如果最后两个字符是()
,则在dp[i-2]
的基础上直接加上2,也就是()
的长度即可;
如果最后两个字符是))
,也就是需要看是否有和最后一个)
对应的(
,也就是第i-1 -dp[i-1]
个字符是否为(
;
))
所对应子串长度dp[i-1] + 2
,再加上完整的))
之前的最长子串长度dp[i-1 -dp[i-1] -1]
,相当于是拼接了两个子串;dp[i]=0
;当然还要注意往前查找的时候,计算出的数组下标是否越界(小于0);
另外需要额外记录最大值,因为dp数组的含义不是问题所求的解;
空间复杂度是O(N),时间复杂度是O(N);
(2) 也可以使用左右括号的计数器来处理,是类似于双指针的思路:
当左右括号的计数相同时,子串有效;
当右括号数量大于左括号数量时,子串失效,所有的计数归零,然后左指针要指向右指针同步;
然而这样遍历一次是不能统计出((()
里面的()
的,因此还要倒序再遍历一次;
仍然是左右括号计数相同时,子串有效;
当左括号数量大于右括号数量时,子串失效,所有的计数归零,然后右指针要指向左指针同步;
这样遍历一次不能统计出()))
中的()
,但这种情况已经在上面的从左到右遍历中统计过了;
注意滞后的指针如果是初始时指向后一位可以方便一点,虽然这一位可能已经超出字符串范围了;
虽然两次遍历有统计上的重复,但由于是找最大值,所以无妨;
空间复杂度是O(1),时间复杂度是O(2N);
推荐还是用动态规划来写,比较优雅一点( ̄︶ ̄)↗;
代码:
(1) 动态规划:
class Solution {
public:
/*
dp[i]:以第i个字符结尾的最长子串长度
// ()()(
(1) s[i] = '(', 则dp[i] = 0
// ()()
(2) s[i] = ')' && s[i-1] = '(', dp[i] = dp[i-2] + 2
// )((()()) 7 - 1 - 4 - 1
s[i] = ')' && s[i-1] = ')' && dp[i-1 -dp[i-1]] = '(', dp[i] = dp[i-1 -dp[i-1] -1] + dp[i-1] + 2
*/
int longestValidParentheses(string s) {
vector<int> dp(s.length(), 0);
int re_max = 0;
for(int i=1;i<s.length();++i) {
if(s[i] == '(') {
dp[i] = 0;
}
else {
if(s[i-1] == '(') {
if(i-2 >= 0) {
dp[i] = dp[i-2] + 2;
}
else {
dp[i] = 2;
}
}
else {
// 定位当前)对应的(的位置并检验是否为(
int leftParIndex = i - 1 - dp[i-1];
if(leftParIndex>=0 && s[leftParIndex] == '(') {
if(leftParIndex-1 >= 0) {
// 左括号左边还有字符串,
// dp[leftParIndex-1]: 0 -> )(
// dp[i-1]: 4 -> ()()
dp[i] = dp[leftParIndex-1] + dp[i-1] + 2;
}
else {
// 左括号左边没有字符串
dp[i] = dp[i-1] + 2;
}
}
else {
dp[i] = 0;
}
}
}
// 记录最大值
if(re_max < dp[i]) {
re_max = dp[i];
}
}
return re_max;
}
};
class Solution {
public:
int longestValidParentheses(string s) {
int re_max = 0;
// 从左往右遍历
int leftParNum = 0, rightParNum = 0;
int i = s.length() - 1, j = s.length();
while(i >= 0) {
if(s[i] == '(') {
++leftParNum;
}
if(s[i] == ')') {
++rightParNum;
}
if(leftParNum == rightParNum) {
int subLen = j - i;
if(re_max < subLen) {
re_max = subLen;
}
}
if(leftParNum > rightParNum) {
// 则右边不可能配对了
j = i;
leftParNum = 0;
rightParNum = 0;
}
--i;
}
// 从右往左遍历
leftParNum = 0, rightParNum = 0;
i = 0, j = -1;
while(i < s.length()) {
if(s[i] == '(') {
++leftParNum;
}
if(s[i] == ')') {
++rightParNum;
}
if(leftParNum == rightParNum) {
int subLen = i - j;
if(re_max < subLen) {
re_max = subLen;
}
}
if(leftParNum < rightParNum) {
// 则左边不可能配对了
j = i;
leftParNum = 0;
rightParNum = 0;
}
++i;
}
return re_max;
}
};
class Solution {
public:
/*
dp[i]: 以第i个元素结尾的最大子数组和
dp[i] = max{dp[i-1]+nums[i], nums[i]}
另外要用一个变量存储最大值
*/
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
int re_max = dp[0];
for(int i=1;i<nums.size();++i) {
if(dp[i-1] <= 0) {
dp[i] = nums[i];
}
else {
dp[i] = dp[i-1] + nums[i];
}
if(dp[i] > re_max) {
re_max = dp[i];
}
}
return re_max;
}
};
如果使用深度遍历搜索,时间复杂度是 O ( 2 M N ) O(2^{MN}) O(2MN),远超动态规划的 O ( M N ) O(MN) O(MN);
和剑指offer算法题02中的七、6. 礼物的最大价值十分类似,但比它简单一点;
代码:
动态规划代码如下:
class Solution {
public:
// dp[i][j]: 走到[i][j]有多少种可能
// dp[i][j] = dp[i-1][j] + dp[i][j-1]
// dp[0][j] = 1; 第一行为1
// dp[i][0] = 1; 第一列为1
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 1));
for(int i=1;i<m;++i) {
for(int j=1;j<n;++j) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
class Solution {
public:
// dp[i][j]: 到达[i][j]时的最小路径和
// dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
for(int i=0;i<m;++i) {
for(int j=0;j<n;++j) {
if(i==0 && j==0) {
// 原点
dp[i][j] = grid[i][j];
}
if(i==0 && j!=0) {
// 第一行
dp[i][j] = dp[i][j-1] + grid[i][j];
}
if(i!=0 && j==0) {
// 第一列
dp[i][j] = dp[i-1][j] + grid[i][j];
}
if(i!=0 && j!=0) {
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
}
return dp[m-1][n-1];
}
};
class Solution {
public:
// dp[i]: 到第i阶楼梯有多少种方式
// dp[i] = dp[i-1] + dp[i-2]
// dp[0] = 1; dp[1] = 1;
int climbStairs(int n) {
vector<int> dp(n+1, 0);
dp[0] = 1;
dp[1] = 1;
for(int i=2;i<=n;++i) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
d p [ i ] [ j ] = { m i n ( d p [ i − 1 ] [ j ] + 1 , d p [ i ] [ j − 1 ] + 1 , d p [ i − 1 ] [ j − 1 ] ) , 若 w o r d 1 [ i ] = = w o r d 2 [ j ] m i n ( d p [ i − 1 ] [ j ] + 1 , d p [ i ] [ j − 1 ] + 1 , d p [ i − 1 ] [ j − 1 ] + 1 ) , 若 w o r d 1 [ i ] ! = w o r d 2 [ j ] dp[i][j]=\left\{ \begin{aligned} min(dp[i-1][j] + 1, \ dp[i][j-1] + 1, \ dp[i-1][j-1]), & 若 word1[i] == word2[j] \\ min(dp[i-1][j] + 1, \ dp[i][j-1] + 1, \ dp[i-1][j-1]+1) , &若word1[i] != word2[j] \end{aligned} \right. dp[i][j]={min(dp[i−1][j]+1, dp[i][j−1]+1, dp[i−1][j−1]),min(dp[i−1][j]+1, dp[i][j−1]+1, dp[i−1][j−1]+1),若word1[i]==word2[j]若word1[i]!=word2[j]
dp[i][j]
的含义:
i
的word1
和长度是j
的word2
之间的编辑距离;如果word1[i] == word2[j]
:
dp[i-1][j] + 1
:在长度是i-1
的word1
上再增加一个字符;dp[i][j-1] + 1
:在长度是j-1
的word2
上再增加一个字符;dp[i-1][j]
和dp[i][j-1]
与dp[i][j]
都只相差了一个字符,所以使两个字符串相同是一定要再增加一个字符的;dp[i-1][j-1]
:直接用dp[i-1][j-1]
的编辑距离;如果word1[i] != word2[j]
:
dp[i-1][j-1]+1
:两个字符串都再增加一个字符,这样编辑距离也是+1
;代码:
class Solution {
public:
// dp[i][j]: 长度i的word1和长度j的word2之间的编辑距离
// dp[i][j] = min{dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1]} 若word1[i] == word2[j]
// = min{dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1} 若word1[i] != word2[j]
// dp[0][0] = 0, dp[0][j] = j, dp[i][0] = i
int minDistance(string word1, string word2) {
int m = word1.length();
int n = word2.length();
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for(int i=0;i<=m;++i) {
dp[i][0] = i;
}
for(int j=0;j<=n;j++) {
dp[0][j] = j;
}
for(int i=1;i<=m;++i) {
for(int j=1;j<=n;++j) {
if(word1[i-1] == word2[j-1]) {
dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1);
dp[i][j] = min(dp[i][j], dp[i-1][j-1]);
}
else {
dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1);
dp[i][j] = min(dp[i][j], dp[i-1][j-1]+1);
}
}
}
return dp[m][n];
}
};
相当于是问二叉搜索树(或者说是二叉树)的形状有多少种;
和普通的二叉树相比,限定为二叉搜索树是为了避免考虑固定形状后节点值的排列;
普通二叉树的数量还要在二叉树的基础上增加节点值的遍历,每种形状共有n!
种值的排列;
代码:
class Solution {
public:
/*
动态规划:
dp[i]: i个节点能够组成的二叉搜索树
dp[i]: sum(k->[0, i-1], dp[k] * dp[i-k-1])
dp[k] => 左子树节点
dp[i-k-1] => 右子树节点
即dp[i] = 左右子树节点的组合数的乘积之和
k + i-k-1 = i-1 => 除去根节点的其余节点
dp[0] = 1;
dp[1] = 1;
限制为二叉搜索树的原因是:
1. 一旦选定某个节点为根节点,则它左边的节点和右边的节点的数量和值就都可以确定
2. 相当于是问二叉树的形状有多少种
3. 普通二叉树的数量还要在二叉树的基础上增加节点的遍历,每种形状共有n!种排列
*/
int numTrees(int n) {
vector<int> dp(n+1, 0);
dp[0] = 1;
dp[1] = 1;
for(int i=2;i<=n;++i) {
for(int k=0;k<i;++k) {
dp[i] += dp[k] * dp[i-k-1];
}
}
return dp[n];
}
};
class Solution {
public:
/*
dp[i] = max(prices[i]-min, max)
*/
int maxProfit(vector<int>& prices) {
int min_val = prices[0];
int max_re = 0;
int i;
for(i=1;i<prices.size();++i) {
if(prices[i] < min_val) {
// 更新最低价格
min_val = prices[i];
}
if(prices[i] - min_val > max_re) {
// 更新最大收益
max_re = prices[i] - min_val;
}
}
return max_re;
}
};
dp[i]
意为s
的前i
个字符是否能用字典拼出;class Solution {
/*
动态规划:
1. dp[i] = dp[i-word[j].len] && dp[i-word[j].len:i]==word[j]
2. dp[i]意为s的前i个字符是否能用字典拼出
*/
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.length() + 1, false);
dp[0] = true;
for(int i=1;i<=s.length();++i) {
for(int j=0;j<wordDict.size();++j) {
int len = wordDict[j].length();
if(i-len>=0 && dp[i-len] && wordDict[j]==s.substr(i-len, len)) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
};
str1.compare(str2) == 0
,如果返回值小于0,则有字典序上的str1 < str2
;str1 == str2
,如果有str1 < str2
,则有字典序上的str1 < str2
;nums[i-1]
为结尾的字串的最大和即可推出以nums[i]
结尾的最大和;nums[i]
结尾的最大乘积可能是由两个正数相乘得到的;nums[i]
是正数还是负数;nums[i-1]
的最大值和最小值,分别对应上面的两种情况,这样无论当前的nums[i]
是正数还是负数,都能求得乘积最大值;0
相乘,乘积的绝对值肯定是越来越大的;dp
数组,而用两个(甚至是一个)临时变量存储nums[i-1]
,这样没有这么直观,但可以进一步优化空间复杂度为O(1),但要注意两者的相互调用关系;class Solution {
public:
/*
dp[i]:以nums[i]结尾的最大非空连续子数组乘积
dp_min[i] = min{dp_min[i-1]*nums[i], dp_max[i-1]*nums[i], nums[i]}
dp_max[i] = max{dp_min[i-1]*nums[i], dp_max[i-1]*nums[i], nums[i]}
*/
int maxProduct(vector<int>& nums) {
vector<int> dp_max(nums.size(), 0);
vector<int> dp_min(nums.size(), 0);
dp_max[0] = nums[0];
dp_min[0] = nums[0];
int re_max = dp_max[0];
for(int i=1;i<nums.size();++i) {
dp_max[i] = max(dp_max[i-1]*nums[i], nums[i]);
dp_max[i] = max(dp_max[i], dp_min[i-1]*nums[i]);
dp_min[i] = min(dp_max[i-1]*nums[i], nums[i]);
dp_min[i] = min(dp_min[i], dp_min[i-1]*nums[i]);
if(dp_max[i] > re_max) {
// 记录最大值
re_max = dp_max[i];
}
}
return re_max;
}
};
dp[i]
意为走到第i
房屋时偷窃第i
房屋可得的最高金额;i
个房屋是一定要偷的,则i-1
房屋不能偷;i-2
房屋偷了,则,i-4
后面的如果可以获得更高的金额的话也可以偷,这是因为i-4
和i-2
不冲突,已经包括在i-2
的子问题内了;i-3
房屋如果偷了,则i-5
后面的也已经考虑在内了;i-2
和i-3
选一间房屋来偷即可;dp[i]
的意义不是很直观,推理也不一定对,但这个是我自己想出来的递推关系( •̀ .̫ •́ )✧;class Solution {
public:
/*
dp[i]:走到i房屋时偷窃i房屋可得的最高金额
dp[i] = max(dp[i-2]+nums[i], dp[i-3]+nums[i])
*/
int rob(vector<int>& nums) {
vector<int> dp(nums.size(), 0);
int re_max = 0;
for(int i=0;i<nums.size();++i) {
if(i <= 1) {
dp[i] = nums[i];
}
else {
dp[i] = max(dp[i], dp[i-2]+nums[i]);
if(i >= 3) {
dp[i] = max(dp[i], dp[i-3]+nums[i]);
}
}
re_max = max(re_max, dp[i]);
}
return re_max;
}
};
是另一种思路,dp[i]
表示经过第i
房屋时可得的最高金额,第i
房屋可以不偷;
则要么偷第i
房屋,金额是第i-2
房屋时的可得最大加上nums[i]
;
要么不偷,金额用第i-1
房屋时的可得最大;
代码二:
class Solution {
public:
/*
dp[i]:走到i房屋时可得的最高金额
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
*/
int rob(vector<int>& nums) {
if(nums.size() == 1) {
return nums[0];
}
if(nums.size() == 2) {
return max(nums[0], nums[1]);
}
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
dp[1] = max(dp[0], nums[1]);
for(int i=2;i<nums.size();++i) {
dp[i] = max(dp[i-1], dp[i-2]+nums[i]);
}
return dp[nums.size()-1];
}
};
class Solution {
private:
struct returnType {
int rob; // 打劫该节点时子树可获得的最大值
int unrob; // 不打劫该节点时子树可获得的最大值
};
returnType dfs(TreeNode *root) {
if(root == nullptr) {
return {0, 0};
}
returnType left = dfs(root->left);
returnType right = dfs(root->right);
// 打劫root,则子节点都不能打劫
int root_rob = root->val + left.unrob + right.unrob;
// 不打劫root,则子节点可以打劫也可以不打劫
int root_unrob = max(left.rob, left.unrob) + max(right.rob, right.unrob);
return {root_rob, root_unrob};
}
public:
int rob(TreeNode* root) {
returnType re = dfs(root);
return max(re.rob, re.unrob);
}
};
class Solution {
public:
/*
dp[i][j]:以matrix[i][j]结尾的正方形的最大边长
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1 (if matrix[i][j]==1)
0 (otherwise)
*/
int maximalSquare(vector<vector<char>>& matrix) {
vector<vector<int>> dp(matrix.size(), vector<int>(matrix[0].size(), 0));
int re_max = 0;
// 初始化第一列和第一行
for(int i=0;i<matrix.size();++i) {
if(matrix[i][0] == '1') {
dp[i][0] = 1;
}
if(dp[i][0] > re_max) {
re_max = dp[i][0];
}
}
for(int j=0;j<matrix[0].size();++j) {
if(matrix[0][j] == '1') {
dp[0][j] = 1;
}
if(dp[0][j] > re_max) {
re_max = dp[0][j];
}
}
// 计算剩下的dp
for(int i=1;i<matrix.size();++i) {
for(int j=1;j<matrix[0].size();++j) {
if(matrix[i][j] == '1') {
dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1;
}
else {
dp[i][j] = 0;
}
if(dp[i][j] > re_max) {
re_max = dp[i][j];
}
}
}
// 返回面积 = 边长*边长
return re_max*re_max;
}
};
n
的每个完全平方数均可以多次使用;n
,因此初始化的时候仅dp[0][0]
或者dp[0]
可以有初始值,其余为INT_MAX
(因为是最小化问题);dp
数组空间为n+1
,两重循环,内循环采用正序遍历;value
,在这里是完全平方数的类型,遍历到sqrt(n)
即可;weight
到n
,weight
在这里是完全平方数占据的空间,即value*value
;n
;class Solution {
public:
/*
dp[i][j]:用前i个完全平方数,和为j的最小数量
dp[i][j] = min(dp[i-1][j], dp[i-1][j-i^2]+1);
降为一维,dp[j] = min(dp[j], dp[j-i^2]+1);
初始值:dp[0] = 0,其余为INT_MAX
*/
int numSquares(int n) {
int max_value = int(sqrt(n));
vector<int> dp(n+1, INT_MAX);
dp[0] = 0;
for(int i=1;i<=max_value;++i) {
int weight = i*i;
for(int j=weight;j<=n;++j) {
dp[j] = min(dp[j], dp[j-weight]+1);
}
}
return dp[n];
}
};
class Solution {
public:
/*
dp[i]:以nums[i]结尾的最长严格递增子序列长度
dp[i] = j:0->i-1 max(dp[j]+1) && nums[j] dp[i]初始值为1
*/
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size(), 1);
int re_max = 1;
for(int i=1;i<nums.size();++i) {
for(int j=0;j<i;++j) {
if(nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j]+1);
// 记录全局最大值
re_max = max(re_max, dp[i]);
}
}
}
return re_max;
}
};
class Solution {
public:
/*
动态规划,每一天结束时有三种状态:
dp[i][0]:持有股票
dp[i][1]:未持有股票且在冷冻期
dp[i][2]:未持有股票且不在冷冻期
*/
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(3, 0));
dp[0][0] = -prices[0]; // 买入当天股票
dp[0][1] = 0;
dp[0][2] = 0;
for(int i=1;i<n;++i) {
// 1. 继续持有或者当天买入
dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i]);
// 2. 今天卖出
dp[i][1] = dp[i-1][0] + prices[i];
// 3. 昨天卖出或者昨天就已经不在冷冻期
dp[i][2] = max(dp[i-1][1], dp[i-1][2]);
}
// 最后一天的2和3的最大值一定大于1
int re_max = max(dp[n-1][1], dp[n-1][2]);
return re_max;
}
};
dp[0] = 0
,其余为最大值;1
,最大的解也是amount
个硬币,所以用amount
和dp[amount]
比较即可知道当前值是否从INT_MAX
而来(也就是不能恰好装满),当然也可以直接用INT_MAX
来比较;class Solution {
public:
/*
dp[i][j]:用前i个硬币凑j面值的最少硬币数
dp[0][0] = 0; dp[0][j] = INT_MAX;
降为一维:
dp[0] = 0;
dp[j] = min(dp[j], dp[j-weight[i]]+1);
*/
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, INT_MAX);
dp[0] = 0;
for(int i=0;i<coins.size();++i) {
for(int j=coins[i];j<=amount;++j) {
if(dp[j-coins[i]] != INT_MAX) {
// 不是从INT_MAX而来,以防溢出
dp[j] = min(dp[j], dp[j-coins[i]] + 1);
}
}
}
if(dp[amount] == INT_MAX) {
return -1;
}
else {
return dp[amount];
}
}
};
思路:
和17. 零钱兑换很像,也是恰好装满问题;
但初始值不同:
dp[0][0]=1
;dp[0][j]=0
;状态转移公式也不同,为两种取法相加,而非取它们的最大值+1;
注意:
代码:
class Solution {
/*
dp[i][j]:用前i个硬币恰好凑j面值的组合数
dp[0][0] = 1; dp[0][j] = 0;
降为一维:
dp[0] = 1;
dp[j] = dp[j] + dp[j-weight[i]];
*/
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1, 0);
dp[0] = 1;
for(int i=0;i<coins.size();++i) {
for(int j=coins[i];j<=amount;++j) {
dp[j] = dp[j] + dp[j-coins[i]];
}
}
return dp[amount];
}
};
i
个数的1的个数其实就是i-step
个数的i的个数再加1;step
是不超过i
的最大二次幂;dp[i]
一定可以由前面的某个dp+1
而来,因此可以用动态规划;class Solution {
public:
/*
000:0
001:1 = [000] + 1 dp[1] = dp[0] + 1 = dp[i-1] + 1
010:1 = [000] + 1 dp[2] = dp[0] + 1 = dp[i-2] + 1
011:2 = [001] + 1 dp[3] = dp[1] + 1 = dp[i-2] + 1
100:1 = [000] + 1 dp[4] = dp[0] + 1 = dp[i-4] + 1
101:2 = [001] + 1 dp[5] = dp[1] + 1 = dp[i-4] + 1
110:2 = [010] + 1
111:3 = [011] + 1
*/
vector<int> countBits(int n) {
vector<int> dp(n+1, 0);
int step = 1;
for(int i=1;i<=n;++i) {
if(i >= 2*step) {
step *= 2;
}
dp[i] = dp[i-step] + 1;
}
return dp;
}
};
class Solution {
public:
/*
动态规划:0/1背包问题
=> 原问题可以转换为求是否能恰好装满数组和一半的背包
dp[i][j]:前i个元素能否恰好装满j
由于不需要value,即不用求最少/最多的元素数量
所以dp数组的类型可以是bool类型
转移方程:
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]]
转为一维:
dp[j] = dp[j] || dp[j-nums[i]]
*/
bool canPartition(vector<int>& nums) {
if(nums.size() <= 1) {
// 仅一个元素不符合要求
return false;
}
// 求数组和
int num_sum = 0;
for(int i=0;i<nums.size();++i) {
num_sum += nums[i];
}
if(num_sum % 2 == 1) {
// 奇数和也不符合要求
return false;
}
// dp
int target = num_sum / 2; // 背包容量
vector<bool> dp(target+1, false);
dp[0] = true;
for(int i=0;i<nums.size();++i) {
for(int j=target;j>=0;--j) {
if(j-nums[i] >= 0) {
dp[j] = dp[j] || dp[j-nums[i]];
}
}
}
return dp[target];
}
};
(target + sum) / 2
,有可能为负数,因此不能做背包的容量,而negative可以证明它一定非负,因为target
总是小于等于sum
;target
是非法的话,也就是说把所有数组里面的数加起来也没有办法凑出一个target
,则negative
就有可能是负数,因此需要提前判断排除这种情况;class Solution {
public:
/*
假设所有数之和是sum,添+的数之和是positive,添-的数之和是negative,则有
sum = positive + negative
=> target = positive - negative = sum - 2*negative = 2*positive - sum
由于target和sum已知,因此positive和negative均可以算出来
=> negative = (sum - target) / 2
=> positive = (target + sum) / 2
=> 也就是原问题等价于能不能用nums中的元素恰好凑出positive或者negative
=> 但由于target <= sum且target可以为负数,因此negative一定非负,但positive有可能是负数
=> 所以只能用negative作为背包的容量
因此转换成一个0/1背包问题
dp[i][j]:用前i个数恰好能凑出j的组合数
转移方程:
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
初始化:
dp[0][0] = 1
*/
int findTargetSumWays(vector<int>& nums, int target) {
int num_sum = 0;
for(int i=0;i<nums.size();++i) {
num_sum += nums[i];
}
if(num_sum < target) {
// sum一定会大于等于target,否则非法
return 0;
}
if((num_sum - target) % 2 == 1) {
// positive不是整数,则target非法
return 0;
}
int negative = (num_sum - target) / 2;
vector<int> dp(negative+1, 0);
dp[0] = 1;
for(int i=0;i<nums.size();++i) {
for(int j=negative;j>=nums[i];--j) {
dp[j] = dp[j] + dp[j - nums[i]];
}
}
return dp[negative];
}
};
一些难以理解的点:
(1) 为什么不用计算i==j
(剩一个气球)和i+1==j
(剩两个气球)的情况下的dp值?
因为按照自定义来看,dp[i][j]
是不含i
和j
的,也就是说至少要三个气球才能计算;
另外,从整个算法来看,剩一个气球和剩两个气球虽然是在子问题中出现了,但它是不符合现实的,因为剩一个气球和剩两个气球总能返回到一个更大的子问题中凑够三个气球再来戳破,而不是在剩一个气球和剩两个气球的时候就把它们戳破;
当然,如果所有的气球加起来也没有三个,才需要讨论戳破一个气球和戳破两个气球的情况;
(2) 为什么要增加一前一后两个伪气球?
一方面,是为了规避所有气球加起来也不够三个的情况的讨论,加上两个伪气球就肯定够三个了;
另一方面,如果气球的数量超过两个,我们是无法知道最后剩下是哪两个气球给我们戳破,这个时候还要循环遍历所有的可能来讨论,为了避免这个麻烦,我们可以加上两个伪气球把这个讨论整合到子问题中来讨论,因为子问题也是做了k
的组合遍历讨论的;
(3) dp数组的填入次序是如何决定?
一个方便的方法是看最后返回的dp下标矩阵的什么位置,这里是返回dp[0][n+1]
,在矩阵的右上角,因此是从下往上遍历,从左往右遍历;
代码:
class Solution {
public:
/*
动态规划:
dp[i][j]:戳破(i:j)之间的所有气球可以获得的硬币最大数量,注意是开区间
1. 转移方程:
dp[i][j] = {k:i+1->j-1}max(dp[i][k] + nums[i]*nums[k]*nums[j] + dp[k][j]) if i+1<=j-1
= nums[i]*nums[j] + max(nums[i], nums[j]) if i+1==j
= nums[i] if i==j
2. 剩两个气球和剩一个气球的情况无需考虑
因为如果nums.size()>=3,则在戳气球的实际过程中不可能会有两个和一个气球的情况
虽然子问题里面会有,但并不处理(戳破),而是直接返回到更大的问题(>=3)中再来戳
3. 由于最后剩下的两个气球可以是数组中的任意两个气球
所以一定要增加一前一后两个伪气球
增加了两个气球之后,也不用讨论剩下的气球数目,因为一定是>=3的
*/
int maxCoins(vector<int>& nums) {
int n = nums.size();
// 增加一前一后两个伪气球,值为1
vector<int> new_nums(n+2, 1);
for(int i=0;i<n;++i) {
new_nums[i+1] = nums[i];
}
vector<vector<int>> dp(n+2, vector<int>(n+2, 0));
for(int i=n+2-1;i>=0;--i) {
for(int j=i;j<n+2;++j) {
if(i == j) {
// 剩一个气球,不讨论
//dp[i][j] = new_nums[i];
}
if(i+1 == j) {
// 剩两个气球,不讨论
//dp[i][j] = new_nums[i] * new_nums[j] + max(new_nums[i], new_nums[j]);
}
if(i+1 <= j-1) {
// 有三个气球
for(int k=i+1;k<=j-1;++k) {
dp[i][j] = max(dp[i][j], dp[i][k] + new_nums[i]*new_nums[k]*new_nums[j] + dp[k][j]);
}
}
}
}
return dp[0][n+1];
}
};
i
和j
两个指针标定当前两个数组已经排除掉的数;i+k/2
和j+k/2
的两个数,并舍弃掉较小的一方的k/2
个数;k/2
就是为了将k个数分到两个数组中;k
中删去k/2
,继续比较;i+k/2
和j+k/2
的两个数均没有越界,则按照上面处理即可;k/2
个数,而是尝试舍弃min(a.size()-i,b.size()-j)
个数;k
个数(而不用再二分取k/2
)即可;i
指针也有可能是j
指针,因此需要用一个变量记录每次移动);class Solution {
private:
/*
findKthSortedArrays:返回两个数组中第rest个数的数值
*/
int findKthSortedArrays(vector<int>& nums1, vector<int>& nums2, int rest) {
int i = -1, j = -1;
double re;
int move;
while(rest > 0) {
if(rest == 1) {
move = 1;
}
else {
move = rest / 2; // 取一半步长则较小的一方必定都是在中位数左边的
}
if(i+move < nums1.size() && j+move <nums2.size()) {
// 按照rest的一半前进
if(nums1[i+move] > nums2[j+move]) {
// nums2可以前进
j += move;
re = nums2[j];
}
else {
// nums1可以前进
i += move;
re = nums1[i];
}
rest -= move;
}
else {
move = min(nums1.size()-i-1, nums2.size()-j-1);
if(move == 0) {
// 直接取另一个数组的第rest个数
if(i == nums1.size() - 1) {
// nums2可以前进
j += rest;
re = nums2[j];
}
else {
// nums1可以前进
i += rest;
re = nums1[i];
}
rest = 0;
}
else {
// 按照最短的move前进
if(nums1[i+move] > nums2[j+move]) {
// nums2可以前进
j += move;
re = nums2[j];
}
else {
// nums1可以前进
i += move;
re = nums1[i];
}
rest -= move;
}
}
}
return re;
}
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int len = nums1.size() + nums2.size();
int rest = len / 2;
if(len % 2 == 0) {
// 中位数有两个,取平均
return 1.0 * (findKthSortedArrays(nums1, nums2, rest) + findKthSortedArrays(nums1, nums2, rest+1)) / 2;
}
else {
// 中位数有一个,直接返回
return 1.0 * findKthSortedArrays(nums1, nums2, rest+1);
}
}
};
target
位置的判断,所以可以和有序数组一样用二分法处理;target
不能判断target
一定在有序序列中,因为在无序的一侧可能会有更大值;实现代码时,因为是用了二分法,所以要用low = mid + 1
,虽然这道题写low = mid
也不会死循环,但为了保险起见还是+1
的写法比较好;
代码:
class Solution {
public:
/*
主要是利用了有序的一侧来进行判断,所以可以和有序数组一样用二分法处理
1. 有可能一侧有序一侧无序,也有可能两侧均有序
2. 有序的一侧一定是升序
mid的可能情况如下:
(1) 4,5,6,7(mid),0,1,2
(2) 4,5,6(mid),7,0,1,2
(3) 4,5,6,7,0(mid),1,2
(4) 7(mid),0
*/
int search(vector<int>& nums, int target) {
int low = 0, high = nums.size() - 1;
int re;
while(low < high) {
int mid = low + (high - low) / 2;
if(nums[mid] >= nums[low]) {
// 有序在左侧[low, mid]
if(nums[mid] >= target && nums[low]<=target) {
// [low, target, mid]
high = mid;
}
else {
low = mid + 1;
}
}
else {
// 有序在右侧[mid, high]
if(nums[mid] < target && nums[high] >= target) {
// (mid, target, high]
low = mid + 1;
}
else {
high = mid;
}
}
}
if(nums[low] == target) {
return low;
}
else {
return -1;
}
}
};
target
的下标;target
的下标;target
的下标;target
的下标;low
和high
时均使用的是mid
的值,不要错用了low
或者high
的值;class Solution {
private:
// 搜紧确下界,第一个为target的值
int searchLowerBound(vector<int>& nums, int target) {
int low = 0, high = nums.size() - 1;
while(low < high) {
int mid = low + (high - low) / 2;
if(nums[mid] < target) {
low = mid + 1;
}
else {
high = mid;
}
}
if(nums[low] == target) {
return low;
}
else {
// 不存在第一个为target的值
return -1;
}
}
// 搜上界,第一个大于target的值
int searchUpperBound(vector<int>& nums, int target) {
int low = 0, high = nums.size() - 1;
while(low < high) {
int mid = low + (high - low) / 2;
if(nums[mid] <= target) {
low = mid + 1;
}
else {
high = mid;
}
}
if(nums[low] > target) {
return low;
}
else {
// 不存在第一个大于target的值
return -1;
}
}
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) {
return {-1, -1};
}
int low = searchLowerBound(nums, target);
int high = searchUpperBound(nums, target);
if(low == -1) {
return {-1, -1};
}
else {
if(high == -1) {
return {low, int(nums.size())-1};
}
else {
return {low, high - 1};
}
}
}
};
思路:
其实是挺难理解的一道题;
比较核心的点如下:
[0, n]
,值域是[1, n]
,基本上是同域的;时间复杂度是O(NlogN),空间复杂度是O(1);
比较关键的点是:
cnt
是小于等于nums[i]
的数量,一定要包括等于;cnt > nums[i]
的最小nums[i]
;一些推导见代码部分;
代码一:
class Solution {
public:
/*
[1, n]中只有一个重复的数,共有n+1个数
1 3 4 2 2
nums[i]: 1 2 3 4
cnt: 1 3 4 5
假设当前数是nums[i],则统计小于等于nums[i]的数
1. 若less_cnt <= nums[i],则nums[i]不是重复的数,而且小于重复的数
==:意味着小于nums[i]的数没有缺失
<: 意味着小于nums[i]的数有缺失,但一定没有重复,因为只能有一个数重复
2. 若less_cnt > nums[i],则最小的nums[i]是重复的数,其余的是大于重复的数
因为前提是:
1. 仅有一个数重复;
2. 共有n个数;
3. 数均在[1, n]中;
于是问题转化为找第一个满足less_cnt > nums[i]的nums[i]
本来是需要先排序在做二分查找的,但这里限制了不能修改数组,又有额外条件:
1. 数组下标是[0, n],而且数组下标是有序的
所以可以用数组下标的[1, n]代替nums[i]作为二分查找的左右端
*/
int findDuplicate(vector<int>& nums) {
int low = 1, high = nums.size()-1;
while(low < high) {
int mid = low + (high-low)/2;
int less_cnt = 0;
for(int i=0;i<nums.size();++i) {
if(nums[i] <= mid) {
++less_cnt;
}
}
if(less_cnt <= mid) {
low = mid + 1;
}
else {
high = mid;
}
}
return low;
}
};
0
可以看作是伪头节点;class Solution {
public:
/*
有n+1个数,取值[1, n],下标[0, n]
推导如下:
1. 假设这n+1个数都不重复,取值在[1, n+1],并把每一个数看做一个节点;
2. 则除了0和n+1外,所有的节点的入度为1(下标[0, n]),出度也为1(取值[1, n+1]且不重复);
3. 0的出度为1,入度为0,n+1的入度为1,出度为0;
4. 按照以上的推导可知,此时所有节点必定是以0为头节点,以n+1为尾节点的有向无环图;
5. 将n+1节点的入度换到[1, n]的任一节点中,则必定会出现环,且该节点就是所求的重复整数;
如果有多个重复值,则:
6. 在上述基础上令某个节点的入度为0,重复值节点增加一个入度,相当于断开某个节点重连;
7. 在环外断开则环内不变,环外路径变短,在环内断开则环外不变,环内路径变短;
8. 此时必定仍有且只有一个环,只是某些节点可能无法从0开始遍历到;
9. 但环是一定可以从0开始遍历到的;
因此,可以转换为求有向有环图的首个入环节点问题,用快慢指针来求解,0是伪头节点;
快慢指针求入环点的推导如下:
1. fs = ss + n*circle = 2*ss;
=> ss = n*circle;
2. ss = a(环外) + b(环内);
=> ss再走a就可以凑够整环回到入环处,从头走a也可以到入环处;
*/
int findDuplicate(vector<int>& nums) {
int fast = 0, slow = 0;
while(fast==0 || fast!=slow) {
slow = nums[slow];
fast = nums[fast];
fast = nums[fast];
}
int slow2 = 0;
while(slow != slow2) {
slow = nums[slow];
slow2 = nums[slow2];
}
return slow;
}
};
x xor x = 0
,x xor 0 = x
;class Solution {
public:
int singleNumber(vector<int>& nums) {
int result = 0;
for(int i=0;i<nums.size();++i) {
result ^= nums[i];
}
return result;
}
};
x = x & (x-1)
加快效率;class Solution {
public:
int hammingDistance(int x, int y) {
int n = x ^ y;
int re = 0;
// 统计异或结果中1的个数
while(n != 0) {
++re;
n = n & (n - 1);
}
return re;
}
};
i
快速移动;class Solution {
public:
/*
双指针:
i指向子串前一位,j指向字串后一位
因此子串长度 = j - i - 1
使用unordered_map的时候注意:
若出现了map[key]的形式,则无论是读取还是写入,都将会为不存在的key创建
之后的map.find将返回true
所以测试输出时应当小心使用printf("%d\n", map[key]);
*/
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> map;
int i = -1, j = 0;
int re = 0;
while(j < s.length()) {
if(map.find(s[j])!=map.end() && (map[s[j]]>=i && map[s[j]]<j)) {
// 更新长度
re = max(re, j - i - 1);
// 移动指针i
i = map[s[j]];
}
map[s[j]] = j;
// 移动指针j
++j;
}
// 最后一个无重复子串也要判断
re = max(re, j - i - 1);
return re;
}
};
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0, j = height.size() - 1;
int re_max = 0;
while(i < j) {
// 用短板的高度 * 宽度
int area = min(height[i], height[j]) * (j - i);
// 记录面积最大值
if(area > re_max) {
re_max = area;
}
// 将短板往中间移动
if(height[i] > height[j]) {
--j;
}
else {
++i;
}
}
return re_max;
}
};
i
处能接的雨水,不考虑它两边的容器形状;i
处能接的雨水 = 它两边最大容器高度的更低一方 - height[i]
;i
两边容器高度的最大值,可以正向遍历得左边容器最大值,反向遍历得右边容器最大值;class Solution {
public:
/*
正向反向遍历:
1. 对于每个height[i]而言,它能够接到的雨水(仅考虑在i上,不考虑它两边的实际形状)
取决于它两边最大高度的更低一方 - height[i]
2. 因此正向遍历得left_max[i],反向遍历得right_max[i]即可
*/
int trap(vector<int>& height) {
vector<int> left_max(height.size(), 0);
vector<int> right_max(height.size(), 0);
// 正向遍历
for(int i=0;i<height.size();++i) {
left_max[i] = height[i];
if(i-1>=0 && left_max[i]<left_max[i-1]) {
left_max[i] = left_max[i-1];
}
}
// 逆向遍历
for(int i=height.size()-1;i>=0;--i) {
right_max[i] = height[i];
if(i+1<height.size() && right_max[i]<right_max[i+1]) {
right_max[i] = right_max[i+1];
}
}
// 计算接雨水之和
int re_sum = 0;
for(int i=0;i<height.size();++i) {
re_sum += min(left_max[i], right_max[i]) - height[i];
}
return re_sum;
}
};
思路二:双指针
但其实这道题的最优解是用双指针,虽然不太直观,但是本质思路也是对思路一的改进;
通过双指针,避免了对left_max[i]
和right_max[i]
的两次遍历求解;
因为计算接雨水的量时,本质上并不是需要把left_max[i]
和right_max[i]
都求出来,而仅需要它们之间的较小值即可,这样就提供了可优化的空间(但仍然是十分巧妙的双指针,比较难想到);
一些推导的过程见下面代码的注释部分;
时间复杂度降至O(N),空间复杂度降至O(1);
代码二:
class Solution {
public:
/*
双指针:
1. 对于每个height[i]而言,它能够接到的雨水(仅考虑在i上,不考虑它两边的实际形状)
取决于min(left_max[i], right_max[i]) - height[i]
实际上,可以用双指针巧妙地替代计算left_max[i]和right_max[i]的两次遍历
2. 定义左指针i,右指针j,i及之前的最大值left_max,j及之后的最大值right_max
3. 对于i而言:
left_max[i] = left_max;
right_max[i] >= right_max;
故若left_max < right_max,则必有left_max[i]= left_max;
right_max[j] = right_max;
故若left_max > right_max,则必有left_max[j]>right_max[j]
5. 如此可以计算出所有位置两边最大高度更低的一方,交替移动指针即可
*/
int trap(vector<int>& height) {
int i = 0, j = height.size() - 1;
int left_max = height[i], right_max = height[j];
int re_sum = 0;
while(i != j) {
if(left_max < right_max) {
// i处的雨水可算,移动左指针
re_sum += (left_max - height[i]);
++i;
left_max = max(left_max, height[i]);
}
else {
// j处的雨水可算,移动右指针
re_sum += (right_max - height[j]);
--j;
right_max = max(right_max, height[j]);
}
}
return re_sum;
}
};
思路:
排序后使用双指针可以将复杂度从 O ( N 3 ) O(N^3) O(N3)降至 O ( N 2 ) O(N^2) O(N2);
可以视作是两数之和的升级版,参看剑指offer算法题02中的十、4. 和为s的两个数字,但增加了重复数的判断;
代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 排序
sort(nums.begin(), nums.end());
int k = 0;
vector<vector<int>> re;
while(k < nums.size()) {
if(nums[k] > 0) {
// 剪枝,提前终止
break;
}
// 固定k之后使用双指针
int i = k + 1, j = nums.size() - 1;
while(i < j) {
int sum = nums[k] + nums[i] + nums[j];
if(sum == 0) {
vector<int> temp(3, 0);
temp[0] = nums[k];
temp[1] = nums[i];
temp[2] = nums[j];
re.push_back(temp);
++i;
// 跳过重复值
while(i<j && nums[i]==nums[i-1]) {
++i;
}
}
if(sum < 0) {
// 和不够大
++i;
// 跳过重复值
while(i<j && nums[i]==nums[i-1]) {
++i;
}
}
if(sum > 0) {
// 和太大
--j;
// 跳过重复值
while(i<j && nums[j]==nums[j+1]) {
--j;
}
}
}
++k;
// 跳过重复值
while(k<nums.size() && nums[k]==nums[k-1]) {
++k;
}
}
return re;
}
};
nullptr
时,前面的指针正好是倒数第n个节点;/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *i = head, *j = head;
int count = 0; // i和j相距的节点数
while(j != nullptr) {
j = j->next;
++count;
if(count > n + 1) {
i = i->next;
}
}
if(count == n) {
// i是要删除的节点
head = i->next;
delete i;
}
else {
// i->next是要删除的节点
if(i->next != nullptr) {
// 因为count>0,所以i->next必不为空,故不用else
ListNode *tmp = i->next;
i->next = tmp->next;
delete tmp;
}
}
return head;
}
};
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
// 增加伪头节点
ListNode *fakeHead = new ListNode();
fakeHead->next = head;
ListNode *i = fakeHead, *j = fakeHead;
int count = 0; // i和j相距的节点数
while(j != nullptr) {
j = j->next;
++count;
if(count > n + 1) {
i = i->next;
}
}
// i->next是要删除的节点
if(i->next != nullptr) {
// 因为count>0,所以i->next必不为空,故不用else
ListNode *tmp = i->next;
i->next = tmp->next;
delete tmp;
}
return fakeHead->next;
}
};
需要注意以下方面:
next0
指针不指向元素0和next2
指针不指向元素2,可以用while
来实现;cur
和两个指针交换元素后,如果交换后cur
指向的元素不是1,则该元素仍需再继续处理;cur
指向的元素为1,另外两种元素则分别交换到头和尾;代码:
class Solution {
public:
/*
三指针
1. next0指向连续0的下一个位置,从左到右遍历
2. next2指向连续2的前一个位置,从右到左遍历
3. cur指向处理的当前位置,从左到右遍历
一些规律如下:
1. next0指向的数一定是1或者2
2. next2指向的数一定是0或者1
3. 如果nums[cur] == 1,则不需要交换
4. 如果nums[cur] == 0或者2,则交换到next0或者next2
5. 注意交换后的数如果是1,则不需要进一步处理,否则需要回退cur指针重复处理
*/
void sortColors(vector<int>& nums) {
int next0 = 0, next2 = nums.size() - 1;
int cur = 0;
while(cur < nums.size()) {
if(nums[cur] == 0) {
while(next0 < cur && nums[next0] == 0) {
++next0;
}
if(next0 < cur) {
// next0在cur前面
swap(nums[cur], nums[next0]);
if(nums[cur] == 2) {
// cur回退
--cur;
}
}
}
else {
while(next2 > cur && nums[next2] == 2) {
--next2;
}
if(next2 > cur) {
// next2在cur后面
swap(nums[cur], nums[next2]);
if(nums[cur] == 0) {
// cur回退
--cur;
}
}
}
++cur;
}
}
};
思路:
用的是双指针的滑动窗口,即左右指针均只能从左向右移动;
要点如下:
需要用两个哈希表,一个记录小串的字符和出现次数,一个记录大串滑动窗口内的字符和出现次数;
在大串中移动滑动窗口时:
注意左指针移动时可以和右指针重合;
代码:
class Solution {
public:
/*
1. 移动right,直到全部全部字符均能覆盖
2. 移动left,直到某个字符不能覆盖
3. 记录此时的right - left
*/
string minWindow(string s, string t) {
unordered_map<char, int> t_map, s_map;
int min_length = s.length() + 1;
string re;
// 初始化两个map
// t_map用于记录, 后续不再修改;s_map用于计数,后续修改
for(int i=0;i<t.length();++i) {
if(t_map.find(t[i]) == t_map.end()) {
t_map[t[i]] = 1;
s_map[t[i]] = 0; // s_map仅初始化
}
else {
t_map[t[i]] += 1;
}
}
int left = 0, right = 0;
int count = 0;
// 在s上移动滑动窗口
while(right < s.length()) {
if(t_map.find(s[right]) != t_map.end()) {
// t中含有右指针字符
s_map[s[right]] += 1;
if(s_map[s[right]] == t_map[s[right]]) {
// 符合条件的字符计数 + 1
++count;
}
}
if(count == t_map.size()) {
// 全部找齐了
while(left <= right) {
if(t_map.find(s[left]) != t_map.end()) {
// t中含有左指针字符
s_map[s[left]] -= 1;
// 检验是否已不能覆盖
if(s_map[s[left]] < t_map[s[left]]) {
--count;
// 记录最小子串
if(min_length > right-left+1) {
min_length = right-left+1;
re = s.substr(left, min_length);
}
// break前记得再移动一次左指针
++left;
break;
}
}
// 移动左指针
++left;
}
}
// 移动右指针
++right;
}
return re;
}
};
head
节点上,快指针每次都比慢指针多走一步;/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==nullptr || head->next == nullptr) {
// 没有节点或仅有一个节点,则不可能成环
return false;
}
ListNode *fast = head;
ListNode *slow = head;
while(fast!=nullptr && slow!=nullptr) {
slow = slow->next;
fast = fast->next;
if(fast != nullptr) {
// fast指针每次多走一步
fast = fast->next;
}
if(slow == fast) {
return true;
}
}
return false;
}
};
slow
指针从head
出发,直到和当前的slow
指针相遇;slow
指针在入环的第一圈内就能与fast
相遇的一些通俗解释:n
是指从环的角度来看,当前slow
在fast
前面n
步;class Solution {
public:
/*
a:从head到第一个环节点经过的节点
b:一个环的节点
第一次相遇的时候,fast比slow多走n圈,有:
slow: s
fast: f = 2*s = s + nb
得s = nb
如果要到第一个环节点,需要走:a + nb
因此slow再走a个节点就到第一个环节点,这也正好是从head到第一个环节点距离
所以还需要一个new_slow从head开始与slow第二次相遇
*/
ListNode *detectCycle(ListNode *head) {
if(head == nullptr || head->next==nullptr) {
// 空节点或者只有一个节点都不可能成环
return nullptr;
}
ListNode *fast = head;
ListNode *slow = head;
while(fast!=nullptr && slow!=nullptr) {
slow = slow->next;
fast = fast->next;
if(fast != nullptr) {
fast = fast->next; // fast多走一步
}
if(slow == fast) {
// 存在环,找第一个入环的节点
fast = head; // fast充当new_slow
while(fast != slow) {
// 做第二次相遇
fast = fast->next;
slow = slow->next;
}
return slow;
}
}
return nullptr;
}
};
head
出发,为空则跳到另一个head
;class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *pa = headA, *pb = headB;
while(pa != pb) {
if(pa != nullptr) {
pa = pa->next;
}
else {
pa = headB;
}
if(pb != nullptr) {
pb = pb->next;
}
else {
pb = headA;
}
}
return pa;
}
};
思路:
当然可以使用类似冒泡排序的思路,时间复杂度是O(NlogN);
但更巧妙的是使用双指针,时间复杂度可以降为O(N);
代码:
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int p1 = 0, p2 = 0;
while(p1 < nums.size() && nums[p1] != 0) {
// p1移动到第一个0处,左侧全为非0
++p1;
}
p2 = p1;
while(p2 < nums.size()) {
// p2继续向右走,遇到非0则和p1交换值
if(nums[p2] != 0) {
nums[p1] = nums[p2];
++p1;
}
++p2;
}
while(p1 < nums.size()) {
// 将剩余的值填0
nums[p1] = 0;
++p1;
}
}
};
cnt_map
,一个统计当前滑动窗口能匹配的字符数量map
;i
和右指针j
:
s[j]
是p中的字符,且map[s[j]]
还没有放满,则将该字符放入map
中,并++j
;map
完全匹配cnt_map
,则还要++i
,让map
空出位置来进行匹配;s[j]
是p中的字符,但map[s[j]]
已经满了,则移动左指针,同时将s[i]
从map
中取出,直到map
可以放下s[j]
为止;s[j]
不是p中的字符,则移动左指针和右指针到j+1
处(因为s[j]
及之前的子串必定不满足条件),并在移动左指针的过程中把到j
之前的s[i]
从map
中取出;class Solution {
public:
vector<int> findAnagrams(string s, string p) {
unordered_map<char, int> cnt_map, map; // cnt_map用来统计p中字符出现的次数
for(char c:p) {
++cnt_map[c];
}
vector<int> re;
int rest_num = p.length();
int i = 0, j = 0;
while(j < s.length()) {
if(cnt_map[s[j]]>0 && map[s[j]]<cnt_map[s[j]]) {
// 情况1:从map中填入p字符
++map[s[j]];
--rest_num;
// 完整重排p
if(rest_num == 0) {
re.push_back(i);
// 放字符放回map中
--map[s[i]];
++rest_num;
// 移动左指针
++i;
}
// 移动右指针
++j;
}
else {
if(cnt_map[s[j]] == 0) {
// 情况3:p中不存在这个字符
while(i < j) {
// 放字符放回map中
--map[s[i]];
++rest_num;
// 移动左指针
++i;
}
++i;
++j;
}
else {
// 情况2:p中有这个字符但map放不下
while(s[i] != s[j]) {
--map[s[i]];
++rest_num;
++i;
}
++i;
++j;
}
}
}
return re;
}
};
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
int left = 0, right = -1;
int max_num = INT_MIN, min_num = INT_MAX;
for(int i=0;i<nums.size();++i) {
if(max_num <= nums[i]) {
// nums[i]越来越大,表明是正常升序
max_num = nums[i];
}
else {
// nums[i]是乱序,更新乱序的右边界
right = i;
}
}
for(int i=nums.size()-1;i>=0;--i) {
if(min_num >= nums[i]) {
// nums[i]越来越小,表明是正常降序
min_num = nums[i];
}
else {
// nums[i]是乱序,更新乱序的左边界
left = i;
}
}
// 如果right == left,则表明无乱序,因为不会存在长度为1的乱序
// 如果right != left,则表明存在乱序,且乱序的长度必定大于等于2
return right - left + 1;
}
};
思路:
当然可以和求全部前k
个元素一样,用堆来做,实现参考:剑指offer算法题02中的十一、1. 最小的k个数,几乎是一样的,但用小顶堆,需要自定义排序函数,时间复杂度是O(Nlogk);
用一般的排序则时间复杂度至少是O(NlogN);
另外,无论是求第k
个元素还是求前k
个元素也可以用类快排方式来求解,加上随机策略之后平均时间复杂度是O(N),不加的话最差是O(N^2),比用堆的方法更慢;
这里将实现类快排查找方式,实现过程如下:
[low, high]
之间的元素作为pivot
,并把它交换到nums[low]
处,而不是直接选择nums[low]
作为pivot
,这可以避免最差的时间复杂度是O(N^2);pivot
移动到中间位置,左边的值均大于pivot
,右边的值均小于pivot
;pivot
的下标是k-1
,则直接返回其值即是第k
个元素,因为下标是从0开始的;pivot
的下标i
小于k-1
,则只搜索[i+1,high]
,否则,只搜索[low, i-1]
,也就是相比于快排只搜索一边;一些需要注意的点:
pivot
是从nums[low]
开始的,所以指针的移动应该先移动j
,从high
向左移;while
循环,直至i
和j
指针相遇;while
和if
判断都应该有i这个条件;
pivot
重新赋值;swap
函数,则直接用引用+临时变量即可;代码:
class Solution {
private:
int re;
void my_swap(int &a, int &b) {
// 用引用+临时变量即可
int temp = a;
a = b;
b = temp;
}
void quickFind(vector<int>& nums, int low, int high, int& k) {
if(low > high) {
// 和quickSort不一样,等号的时候也要走一遍,不然i==k-1可能无法取得
return;
}
int i = low, j = high;
// 引入随机选择pivot,能够避免最坏为O(n^2),整体是O(n)
int rand_index = low + rand()%(high-low+1);
my_swap(nums[low], nums[rand_index]);
// 往下是快排的写法
int pivot = nums[low];
while(i < j) {
// 把pivot放到中间,前大后小
while(i<j && pivot>=nums[j]){
--j;
}
if(i < j) {
nums[i] = nums[j]; // nums[j] = pivot
++i;
}
while(i<j && pivot<=nums[i]) {
++i;
}
if(i < j) {
nums[j] = nums[i]; // nums[i] = pivot
--j;
}
}
// 重新赋值
nums[i] = pivot;
if(i == k-1) {
re = nums[i];
return;
}
else {
// k-1在[low, i-1]中
if(i > k-1) { quickFind(nums, low, i-1, k); }
// k-1在[i+1, high]中
if(i < k-1) { quickFind(nums, i+1, high, k); }
}
}
public:
int findKthLargest(vector<int>& nums, int k) {
quickFind(nums, 0, nums.size()-1, k);
return re;
}
};
k
个数;unordered_map
来遍历一遍统计即可;priority_queue
是大顶堆,需要进一步改造;quickFind()
函数,注意返回类型和快排一样也必须是void
类型,参数包括数组arr
,下标low
和high
,寻找的位置k
,用递归实现;low == high
;while
移动指针的时候注意等于的时候也需要移动;pivot
策略,否则时间复杂度的期望不能到O(N);i == k-1
,如果用k
而且k == arr.size()
时是找不到的,因为此时的k
不在数组的下标范围内;class Solution {
private:
vector<int> re;
void quickFind(vector<pair<int, int>>& arr, int low, int high, int k) {
// 等号也必须取到
if(low > high) {
return;
}
// 取随机数,[low, high]间有high-low+1个元素,1要加上
int rand_index = low + rand()%(high - low + 1);
swap(arr[low], arr[rand_index]);
pair<int, int> pivot = arr[low];
int i = low, j = high;
while(i < j) {
// 注意等于号
while(i<j && arr[j].second<=pivot.second) {
--j;
}
if(i<j) {
arr[i] = arr[j];
++i;
}
// 注意等于号
while(i<j && arr[i].second>=pivot.second) {
++i;
}
if(i<j) {
arr[j] = arr[i];
--j;
}
}
arr[i] = pivot;
// 一定要i==k-1,而不能i==k,否则如果k==arr.size()则是找不出的
if(i == k-1) {
//printf("k=%d\n", i);
for(int index=0;index<=i;++index) {
re.push_back(arr[index].first);
}
return;
}
else {
if(i < k) {
quickFind(arr, i+1, high, k);
}
else {
quickFind(arr, low, i-1, k);
}
return;
}
}
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> count_map;
// 统计出现的次数
for(int i=0;i<nums.size();++i) {
if(count_map.find(nums[i]) == count_map.end()) {
count_map[nums[i]] = 0;
}
++count_map[nums[i]];
}
// 转存到数组
vector<pair<int, int>> count_arr;
for(auto i=count_map.begin();i!=count_map.end();++i) {
count_arr.push_back({i->first, i->second});
}
// 类快排查找
// 注意上下界均是紧确界
quickFind(count_arr, 0, count_arr.size()-1, k);
return re;
}
};
k
个元素用小顶堆;k
个元素用大顶堆;class Solution {
private:
// 仿函数
struct cmp {
bool operator() (const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
}
};
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> count_map;
// 统计出现的次数
for(int i=0;i<nums.size();++i) {
if(count_map.find(nums[i]) == count_map.end()) {
count_map[nums[i]] = 0;
}
++count_map[nums[i]];
}
// 转存到小顶堆
priority_queue<pair<int, int>, vector<pair<int, int>>, cmp> heap;
for(auto i=count_map.begin();i!=count_map.end();++i) {
if(heap.size() < k) {
heap.push({i->first, i->second});
}
else {
pair<int, int> tmp = heap.top();
if(i->second > tmp.second) {
heap.pop();
heap.push({i->first, i->second});
}
}
}
// 记录到数组
vector<int> re;
while(!heap.empty()) {
re.push_back(heap.top().first);
heap.pop();
}
return re;
}
};
decltype()
函数,用于返回传入参数的类型;cmp
是一个静态函数;class Solution {
private:
static bool cmp(const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
}
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> count_map;
// 统计出现的次数
for(int i=0;i<nums.size();++i) {
if(count_map.find(nums[i]) == count_map.end()) {
count_map[nums[i]] = 0;
}
++count_map[nums[i]];
}
// 转存到小顶堆
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> heap(cmp);
for(auto i=count_map.begin();i!=count_map.end();++i) {
if(heap.size() < k) {
heap.push({i->first, i->second});
}
else {
pair<int, int> tmp = heap.top();
if(i->second > tmp.second) {
heap.pop();
heap.push({i->first, i->second});
}
}
}
// 记录到数组
vector<int> re;
while(!heap.empty()) {
re.push_back(heap.top().first);
heap.pop();
}
return re;
}
};
sort
函数的cmp
可以用函数指针,也可以用仿函数,但用仿函数的时候用的是T()
的形式而不是T
,实际上传入的参数仍然是函数指针而不是类对象,参考博客:C++ 仿函数和自定义排序函数的总结;// 仿函数
struct cmp {
bool operator() (const T& a, const T& b) {
return a严格小于b的条件;
}
};
priority_queue
的定义如下:priority_queue<T, vector<T>, cmp> heap;
priority_queue
是vector
的配接器,所以要显式给出vector
的类型;cmp()
,而是直接传入整个仿函数类类型cmp
;int
,也可以直接用STL中的标准greater
仿函数,用法如下:priority_queue<int, vector<int>, greater<int>> heap;
思路:
本来还打算用动态规划来做的,但动态规划时间复杂度是 O ( N 2 ) O(N^2) O(N2),因为对每个i
判断dp[i]
是否可达,都要遍历它前面的位置看是否有机会到i
;
其实直接用贪心法就可以了,时间复杂度是 O ( N ) O(N) O(N);
核心点:
true
;代码:
class Solution {
public:
bool canJump(vector<int>& nums) {
int max_reach = 0;
int cur = 0;
while(cur <= max_reach && cur < nums.size()) {
// 仅当cur处于最大可达距离内才遍历
max_reach = max(cur + nums[cur], max_reach);
++cur;
}
if(max_reach >= nums.size()-1) {
return true;
}
else {
return false;
}
}
};
思路:
先排序,再顺次遍历合并;
合并 [ L 1 , R 1 ] [L_1,R_1] [L1,R1]和 [ L 2 , R 2 ] [L_2,R_2] [L2,R2]时:
代码:
class Solution {
public:
// 必须是static,因为是成员函数,不用static的话不能在sort中使用
// 但如果不是成员函数的话就不需要用static
// 传入的参数用引用的话时间和空间都能极大地节省
static bool cmp(vector<int>& a, vector<int>& b) {
if(a[0] < b[0]) {
return true;
}
if(a[0] > b[0]) {
return false;
}
else {
if(a[1] < b[1]) {
return true;
}
else {
return false;
}
}
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
vector<vector<int>> re;
int i = 0;
while(i < intervals.size()) {
vector<int> tmp = intervals[i];
++i;
while(i < intervals.size() && tmp[1] >= intervals[i][0]) {
// 注意比较前一个区间的右区间和下一个区间的右区间的大小
tmp[1] = max(tmp[1], intervals[i][1]);
++i;
}
re.push_back(tmp);
}
return re;
}
};
bool
类型返回值;a
和b
应当用const
和引用&
修饰,以减少空间和时间开销(很重要!);true
代表a
排在b
前面;static
+ private
修饰,否则则不需要;vector
和string
类型都可以用sort
进行排序;true
的时候必须是严格小于,=
的情况下不能返回true
值,否则存在相等值时递归的过程可能会陷入死循环或者空递归;[h_i, k_i]
;h_i
来从高到低排序;k_i
个位置上;i
个人来说,必定有k_i <= i
,也就是说插入的操作是往前面已经排好的队伍里面插入;i
个人来说,这样的插入一定是满足条件的,因为前面的人都比它高,所以它放在哪个位置就k_i
是多少;i
个人比它们矮,所以无论第i
个人插入到哪个位置都不会改变它们原来的k_i
值,所以也满足条件;list
容器来进行;(*i)
,需要加括号,而取指针i->
等价于先取(*i)
再(*i)->
;for
循环的终止条件用的是!=container.end()
;list.insert()
的第一个参数是迭代器,必须通过list.begin()
自增(迭代器自增已重载)得到;list
底层是双向环状链表,本身也不支持随机读取的逻辑;vector
的迭代器,因为它的空间本来就是连续的,而且迭代器本质上是普通指针,所以用vector.begin() + int
的形式也可以;class Solution {
private:
static bool cmp(const vector<int>& a, const vector<int>& b) {
if(a[0] > b[0]) {
return true;
}
else {
if(a[0] == b[0] && a[1] < b[1]) {
return true;
}
}
return false;
}
public:
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
// 排序people
sort(people.begin(), people.end(), cmp);
list<vector<int>> re_list;
for(auto i=people.begin();i!=people.end();++i) {
// 顺序遍历,并依次把每个元素插入到第k_i个位置
int tmp = (*i)[1];
auto tmp_it = re_list.begin();
while(--tmp >= 0) {
++tmp_it;
}
re_list.insert(tmp_it, *i); // insert函数是在position之前插入
// print_info
// for(auto j=re_list.begin();j!=re_list.end();++j) {
// printf("[%d, %d], ",(*j)[0], (*j)[1]);
// }
// printf("\n");
}
vector<vector<int>> re;
for(auto i=re_list.begin();i!=re_list.end();++i) {
re.push_back(*i);
}
return re;
}
};
class Solution {
public:
/*
贪心法:
1. 先找出执行次数最多的任务,记录次数为k,如果有并列最多的,记录并列数为x
2. 构建一个k行n+1列的矩阵,且最后一行仅有x个任务,这样:
(1) 只要每一行内的任务类型不重复,则不会发生有任务处于待命状态而冲突
(2) 相同类型的任务填入不同行也不会发生冲突
(3) 如果矩阵填不满,则所需要的最短时间不会变,仍然是填满矩阵的时间
(4) 如果矩阵填满而且超出,则每一行可以增加列来填入
也就是能够安排一种方案让所有任务都不需要等待执行
3. 则完成任务的时间为:
(1) (k-1) * (n+1) + x, if 填不满或刚好填满
(2) tasks.size(), if 超出
*/
int leastInterval(vector<char>& tasks, int n) {
// 桶计数
vector<int> bucket_count(26, 0);
int k = 0; // 执行次数最多的任务的执行次数
int x = 0; // 有并列最多的任务的并列数
for(int i=0;i<tasks.size();++i) {
++bucket_count[tasks[i]-'A'];
if(bucket_count[tasks[i]-'A'] > k) {
k = bucket_count[tasks[i]-'A'];
x = 1;
}
else {
if(bucket_count[tasks[i]-'A'] == k) {
++x;
}
}
}
// 注意size()返回的是unsigned_int类型,需要做类型转换
return max(int(tasks.size()), (k - 1) * (n + 1) + x);
}
};
思路:
本质是判断图是不是一个有向无环图;
等价于能不能从图中获得一个拓扑排序;
本质上就是不断找当前入度为0
的节点然后解除它们的入度;
0
是通过队列来实现的,所以相当于是广度优先遍历;使用的数据结构包括:
vector
:记录每个节点对应的出节点;
vector
:记录每个节点的入度;
0
的节点,所以还要再记录入度值;这样实现的时间复杂度是最低的,而且也比较容易理解;
代码:
队列实现版本:
class Solution {
public:
/*
1. 本质上是判断当前有向图中是否存在环
2. 可以转换成是否能够从有向图中获得一个拓扑排序
如果可以获得,则无环,反之则有环;
3. 拓扑排序是:
1) 每次取入度为0的节点
2) 直至全部取完,则无环;
3) 或无入度为0的节点但未取完,则有环;
*/
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> out_nodes(numCourses);
vector<int> in_num(numCourses, 0);
for(auto p: prerequisites) {
out_nodes[p[1]].emplace_back(p[0]);
++in_num[p[0]];
}
queue<int> q;
for(int i=0;i<numCourses;++i) {
if(in_num[i] == 0) {
q.emplace(i);
}
}
vector<int> re;
while(!q.empty()) {
int delete_node = q.front();
q.pop();
re.emplace_back(delete_node);
// 遍历所有出节点,删除它们的入度
for(auto out_node: out_nodes[delete_node]) {
--in_num[out_node];
if(in_num[out_node] == 0) {
q.emplace(out_node);
}
}
}
if(re.size() == numCourses) {
return true;
}
else {
return false;
}
}
};
思路:
和课程表的思路完全相同,也是用拓扑排序;
只是需要额外记录拓扑排序的顺序再返回;
代码:
class Solution {
public:
/*
1. 本质上是判断当前有向图中是否存在环
2. 可以转换成是否能够从有向图中获得一个拓扑排序
如果可以获得,则无环,反之则有环;
3. 拓扑排序是:
1) 每次取入度为0的节点
2) 直至全部取完,则无环;
3) 或无入度为0的节点但未取完,则有环;
*/
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites)
{
vector<vector<int>> out_nodes(numCourses);
vector<int> in_num(numCourses, 0);
for(int i=0;i<prerequisites.size();++i) {
// 记录每个节点的出节点
out_nodes[prerequisites[i][1]].emplace_back(prerequisites[i][0]);
// 记录每个节点的入度
++in_num[prerequisites[i][0]];
}
queue<int> q;
for(int i=0;i<numCourses;++i) {
if(in_num[i] == 0) {
q.emplace(i);
}
}
vector<int> re;
while(!q.empty()) {
int delete_node = q.front();
re.emplace_back(delete_node);
q.pop();
// 对每个出节点的入度减一
for(int i=0;i<out_nodes[delete_node].size();++i) {
int out_node = out_nodes[delete_node][i];
--in_num[out_node];
if(in_num[out_node] == 0) {
q.emplace(out_node);
}
}
}
if(re.size() == numCourses) {
return re;
}
else {
return {};
}
}
};
class Solution {
public:
/*摩尔投票法*/
int majorityElement(vector<int>& nums) {
int major;
int votes = 0;
for(int i=0;i<nums.size();++i) {
if(votes == 0) {
major = nums[i];
++votes;
}
else {
if(major == nums[i]) {
++votes;
}
else {
--votes;
}
}
}
return major;
}
};
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n = nums.size();
vector<int> pre_multipy(nums.size(), 0); // 到i的前向乘积(含i)
vector<int> post_multipy(nums.size(), 0); // 到j的后向乘积(含j)
// 计算前向乘积
pre_multipy[0] = nums[0];
for(int i=1;i<n;++i) {
pre_multipy[i] = pre_multipy[i-1] * nums[i];
}
// 计算后向乘积
post_multipy[n-1] = nums[n-1];
for(int i=n-2;i>=0;--i) {
post_multipy[i] = post_multipy[i+1] * nums[i];
}
// 计算结果
vector<int> re(n, 0);
re[0] = post_multipy[1];
re[n-1] = pre_multipy[n-2];
for(int i=1;i<n-1;++i) {
re[i] = pre_multipy[i-1] * post_multipy[i+1];
}
return re;
}
};