(动态规划分析步骤:确定dp数组下标及含义,确定dp数组的递推公式,dp数组初始化,确定遍历顺序)
dp数组下标及含义:和122. 买卖股票的最佳时机 II类似,设dp[i][0]
为从nums[0]
,nums[1]
,…,nums[i]
中选择一个子序列,并且该子序列最后一个元素的下标为偶数的最大交替和;设dp[i][1]
为从nums[0]
,nums[1]
,…,nums[i]
中选择一个子序列,并且该子序列最后一个元素的下标为奇数的最大交替和。
递推公式
对于dp[i][0]
可以由两个状态推导:
nums[i]
,则dp[i][0]
=dp[i-1][0]
nums[i]
,则dp[i][0]
=dp[i-1][1]
+nums[i]
对于dp[i][1]
可以由两个状态推导:
nums[i]
,则dp[i][1]
=dp[i-1][1]
nums[i]
,则dp[i][1]
=dp[i-1][0]
-nums[i]
初始化:dp[i][0]
=nums[0]
,dp[i][1]
=0
遍历顺序:因为dp[i]
由dp[i-1]
确定,所以由前到后遍历
最大的交替和的子序列最后一个元素一定是偶数,所以最终返回dp[nums.size() - 1][0]
class Solution {
public:
long long maxAlternatingSum(vector& nums) {
vector> dp(nums.size(), vector(2, 0));
dp[0][0] = nums[0];
dp[0][1] = 0;
for (int i = 1; i < nums.size(); i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + nums[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - nums[i]);
}
return dp[nums.size() - 1][0];
}
};
由题意可知n的最高位符号为正,后面位按负正负正…的顺序,而我们通常通过倒序获得一个整数的各个位的值,因此如果知道最后一位的符号即可按倒序求解交替数字的和。如果n由奇数位组成则最后一位符号为正,由偶数位组成则最后一位符号为负,因此首先求n有多少位(可以先将n转为string,进而获取位数的奇偶性)。
class Solution {
public:
int alternateDigitSum(int n) {
int res = 0;
int flag = to_string(n).size() % 2 == 1 ? 1 : -1;
while (n > 0) {
int num = n % 10;
n /= 10;
res += flag * num;
flag = -flag;
}
return res;
}
};
dp数组下标及含义:dp[i][j]
表示从第0
行到第i
行,第j
列和最小的下降路径长度,最后返回最后一行的和最小下降路径长度的最小值即可
递推公式:dp[i][j]=nums[i][j]+min(dp[i-1][j-1],dp[i-1][j],dp[i-1][j+1])
,第一列和最后一列有边界情况,需要特殊处理。
初始化:dp[0][j]=nums[0][j]
遍历顺序:由左向右,由上到下
class Solution {
public:
int minFallingPathSum(vector>& matrix) {
int n = matrix.size();
vector> dp(n, vector(n, 0));
copy(matrix[0].begin(), matrix[0].end(), dp[0].begin()); // copy函数
for (int row = 1; row < n; row++) {
for (int col = 0; col < n; col++) {
dp[row][col] = dp[row - 1][col];
if (col > 0 ) dp[row][col] = min(dp[row][col], dp[row - 1][col - 1]);
if (col < n - 1) dp[row][col] = min(dp[row][col], dp[row - 1][col + 1]);
dp[row][col] += matrix[row][col];
}
}
return *min_element(dp[n - 1].begin(), dp[n - 1].end()); // vector特殊函数
}
};
dfs(a)
表示以a
为根结点的子树满足每个结点只有一个金币时,a
结点的父结点需要从a
拿走的金币数量node
,val(node)
为该结点的金币数目,设node
的左孩子为left
,右孩子为right
node
需要从left
拿走的金币数目为dfs(left)
,node
和left
之间需要移动|dfs(left)|
次金币;node
需要从right
拿走的金币数目为dfs(right)
,node
和right
之间需要移动|dfs(right)|
次金币。node
结点的金币数目为|dfs(left)|+|dfs(right)|+val(node)
,由于node需要保留一个金币,所以node的根结点需要拿走它的金币数量为|dfs(left)|+|dfs(right)|+val(node)-1
node
时,返回其父节点需要从node
拿走的金币数目,并统计当前结点与其子结点之间移动金币的数目,通过递归遍历即可求得所有结点与其父结点之间移动金币的次数统计之和即为总的金币移动次数/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int res = 0;
int dfs(TreeNode* root) {
if (root == NULL) return 0;
int left = dfs(root->left);
int right = dfs(root->right);
res += abs(left) + abs(right);
return left + right + root->val - 1;
}
int distributeCoins(TreeNode* root) {
dfs(root);
return res;
}
};
和15. 三数之和类似,注意剪枝去重操作,不再详细介绍(可参考代码随想录解析)。
class Solution {
public:
vector> fourSum(vector& nums, int target) {
sort(nums.begin(), nums.end());
vector> res;
for (int k = 0; k < nums.size(); k++) {
if (k > 0 && nums[k] == nums[k - 1]) continue;
for (int i = k + 1; i < nums.size(); i++) {
if (i > k + 1 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
if ((long)nums[k] + nums[i] + nums[left] + nums[right] > target) right--;
else if ((long)nums[k] + nums[i] + nums[left] + nums[right] < target) left++;
else {
res.push_back({nums[k], nums[i], nums[left], nums[right]});
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
}
}
}
}
return res;
}
};
定义两个指针i
、j
,分别指向num1
和num2
的末尾,同时定义add
变量维护是否出现进位,从末尾到开头逐位相加即可(如果指针出现了负值,则补零即可)。
class Solution {
public:
string addStrings(string num1, string num2) {
int i = num1.size() - 1, j = num2.size() - 1, add = 0;
string res = "";
while (i >= 0 || j >= 0 || add != 0) {
int x = i >= 0 ? num1[i] - '0' : 0;
int y = j >= 0 ? num2[j] - '0' : 0;
int result = x + y + add;
res.push_back(result % 10 + '0');
add = result / 10;
i--;
j--;
}
reverse(res.begin(), res.end());
return res;
}
};
直接模拟即可,详情见代码(注意unordered_set
、set
以及unordered_map
、map
不能有重复的key值)
class Solution {
public:
int robotSim(vector& commands, vector>& obstacles) {
unordered_set uset;
for (int i = 0; i < obstacles.size(); i++) uset.insert(obstacles[i][0] * 60001 + obstacles[i][1]);
int x = 0;
int y = 0;
int head = 4; // 东西南北1234
int res = 0;
for (int i = 0; i < commands.size(); i++) {
if ((head == 4 && commands[i] == -1) || (head == 3 && commands[i] == -2)) head = 1;
else if ((head == 4 && commands[i] == -2) || (head == 3 && commands[i] == -1)) head = 2;
else if ((head == 1 && commands[i] == -1) || (head == 2 && commands[i] == -2)) head = 3;
else if ((head == 1 && commands[i] == -2) || (head == 2 && commands[i] == -1)) head = 4;
else if (commands[i] >= 1 && commands[i] <= 9) {
if (head == 1) {
int temp = x + commands[i];
for (int j = x + 1; j <= temp; j++) {
if (uset.find(j * 60001 + y) != uset.end()) {
x = j - 1;
break;
} else x = j;
}
}
if (head == 2) {
int temp = x - commands[i];
for (int j = x - 1; j >= temp; j--) {
if (uset.find(j * 60001 + y) != uset.end()) {
x = j + 1;
break;
} else x = j;
}
}
if (head == 3) {
int temp = y - commands[i];
for (int j = y - 1; j >= temp; j--) {
if (uset.find(x * 60001 + j) != uset.end()) {
y = j + 1;
break;
} else y = j;
}
}
if (head == 4) {
int temp = y + commands[i];
for (int j = y + 1; j <= temp; j++) {
if (uset.find(x * 60001 + j) != uset.end()) {
y = j - 1;
break;
} else y = j;
}
}
res = max(res, x * x + y * y);
}
}
return res;
}
};
本题为53. 最大子数组和的进阶版,最大和的数组存在两种情况:
nums[i]~nums[j]
nums[0]~nums[i]和nums[j]~nums[nums.size() - 1]
,可以转换为求nums[i]~nums[j]
最小子数组和。针对case1,通过i
遍历nums
(可视为数组开头),count1
记录数组和(可以视为末尾),当count1<=0
时,抛弃前面元素,count1
置为0,从i + 1
开始重新计算。
针对case2,先寻找最子数组和:通过i
遍历nums
(可视为数组开头),count2
记录数组和(可以视为末尾),当count2>=0
时,抛弃前面元素,count2
置为0,从i + 1
开始重新计算。之后用sum-最小字数即为最大子数组和。
取case1和case2的较大值为最终结果。得注意的一点,当case1得到的最大子数组和为负数时(即数组中所有元素都为负数),不能用case2的结果(case2的结果为0,此时最小子数组为整个数组,最大子数组就没有元素了,显然不合理)。
class Solution {
public:
int maxSubarraySumCircular(vector& nums) {
int sum = 0;
for (auto n : nums) sum += n;
int res1 = INT_MIN;
int count1 = 0;
int res2 = INT_MAX;
int count2 = 0;
for (int i = 0; i < nums.size(); i++) {
count1 += nums[i];
if (res1 < count1) res1 = count1;
if (count1 <= 0) count1 = 0;
count2 += nums[i];
if (res2 > count2) res2 = count2;
if (count2 >= 0) count2 = 0;
}
if (res1 < 0) return res1;
else return max(res1, sum - res2);
}
};
存储5美元和10美元的数量。遇到5美元就收下;遇到10美元就判断是否有5美元,有的话就收下10美元并找给5美元,否则不能找零return false;遇到20美元就先判断是否有10美元和5美元,再判断是否有3张5美元(上述找零涉及贪心,即尽可能保留5美元),否则不能找零return false。
class Solution {
public:
bool lemonadeChange(vector& bills) {
int five = 0, ten = 0;
for (auto b : bills) {
if (b == 5) five++;
if (b == 10) {
if (five > 0) {
ten++;
five--;
} else return false;
}
if (b == 20) {
if (five > 0 && ten > 0) {
five--;
ten--;
} else if (five >= 3) five -= 3;
else return false;
}
}
return true;
}
};
计算方法:按行来计算雨水体积,使用单调栈,从栈顶到栈底元素应该是从小到大,因为如果将要添加的元素大于栈顶元素,则出现凹槽:栈顶元素是凹槽的底部柱子,栈顶的第二个元素是凹槽左侧柱子,要添加的元素是凹槽右侧柱子。由于通过宽*高来计算雨水的体积,既需要知道元素值,又需要知道元素的下标,因此单调栈中存储元素下标,元素值则通过下标来获得。
单调栈逻辑:
class Solution {
public:
int trap(vector& height) {
stack st;
int res = 0;
st.push(0);
for (int i = 1; i < height.size(); i++) {
if (height[i] < height[st.top()]) st.push(i);
else if (height[i] == height[st.top()]) {
st.pop();
st.push(i);
} else {
while (!st.empty() && height[i] > height[st.top()]) {
int mid = st.top();
st.pop();
if (!st.empty()) {
int h = min(height[st.top()], height[i]) - height[mid];
int w = i - st.top() - 1;
res += h * w;
}
}
st.push(i);
}
}
return res;
}
};
将jewels
由vector
转为unordered_set
哈希表,因为哈希表的查找的时间复杂度很低,只有O(1)
。之后再遍历stones
,判断每个元素是否出现在了jewels
。
class Solution {
public:
int numJewelsInStones(string jewels, string stones) {
unordered_set uset(jewels.begin(), jewels.end());
int res = 0;
for (int i = 0; i < stones.size(); i++) {
if (uset.find(stones[i]) != uset.end()) res++;
}
return res;
}
};
每次减去最大元素值的一半,可以得到最小的操作数。
nums
可以保存为大顶堆的优先级队列priority_queue
(less
表示大顶堆,greater
表示小顶堆,缺省为大顶堆)。在循环函数中,每次通过大顶堆最大元素(top()
)的一半计算减去数的和,并将堆顶元素弹出,压入原堆顶元素值的一半,当减去数的和大于等于原nums
数组元素和的一半时,停止循环。
class Solution {
public:
int halveArray(vector& nums) {
int res = 0;
priority_queue pq(nums.begin(), nums.end());
double sumDiff = 0;
double sumAll = accumulate(nums.begin(), nums.end(), 0.0); // 注意是0.0,不能是0,因为如果是0的话,nums先按int相加,再转变为double,可能会超出int范围
while (sumDiff < sumAll / 2) {
double temp = pq.top() / 2;
pq.pop();
pq.push(temp);
sumDiff += temp;
res++;
}
return res;
}
};
将题意转化为:对数组的每一行排序,排完序后选择每一列的最大值,再求和。
class Solution {
public:
int deleteGreatestValue(vector>& grid) {
int res = 0;
for (int i = 0; i < grid.size(); i++) {
sort(grid[i].begin(), grid[i].end());
}
for (int j = 0; j < grid[0].size(); j++) {
int temp = INT_MIN;
for (int i = 0; i < grid.size(); i++) {
temp = max(temp, grid[i][j]);
}
res += temp;
}
return res;
}
};
定义快慢指针,最初快慢指针都在链表头,之后慢指针一次走一步,快指针一次走两步。不断前进,如果慢指针等于了快指针,则有环,否则无环(如果想要找到环形链表的入口,则再定义两个指针,一个指针指向链表头,另一个指针指向快慢指针的交点处,这两个指针每次都走一步,直至相遇,相遇点即为环形链表的入口,详见142. 环形链表 II)
/**
* 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) {
ListNode* fast = head;
ListNode* slow = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true;
}
return false;
}
};
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slowIndex = head;
ListNode* fastIndex = head;
while (fastIndex != NULL && fastIndex->next != NULL) {
slowIndex = slowIndex->next;
fastIndex = fastIndex->next->next;
if (slowIndex == fastIndex) {
ListNode* tmp1 = head;
ListNode* tmp2 = fastIndex;
while (tmp1 != tmp2) {
tmp1 = tmp1->next;
tmp2 = tmp2->next;
}
return tmp1;
}
}
return nullptr;
}
};
先将链表用线性表存储,之后定义两个指针,一个头指针,一个尾指针(详见代码)
/**
* 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:
void reorderList(ListNode* head) {
vector vec;
ListNode* cur = head;
while (cur != NULL) {
vec.emplace_back(cur);
cur = cur->next;
}
int i = 0, j = vec.size() - 1;
while (i < j) {
vec[i]->next = vec[j];
i++;
vec[j]->next = vec[i];
j--;
}
vec[i]->next = nullptr;
}
};