剑指 Offer II 005. 单词长度的最大乘积
给定一个字符串数组 words
,请计算当两个字符串 words[i]
和 words[j]
不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。
示例 1:
输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]
输出: 16
解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。
解析:
方法一:位运算
为了得到单词长度的最大乘积,朴素的做法是,遍历字符串数组
用 表示数组 的长度,用 表示单词 的长度,其中 ,则上述做法需要遍历字符串数组 中的每一对单词,对于下标为 和 的单词,其中 ,需要 的时间判断是否有公共字母和计算长度乘积。因此上述做法的时间复杂度是 ,该时间复杂度高于 。
如果可以将判断两个单词是否有公共字母的时间复杂度降低到 ,则可以将总时间复杂度降低到 。可以使用位运算预处理每个单词,通过位运算操作判断两个单词是否有公共字母。由于单词只包含小写字母,共有 个小写字母,因此可以使用位掩码的最低 位分别表示每个字母是否在这个单词中出现。将 到 分别记为第 个字母到第 个字母,则位掩码的从低到高的第 位是 当且仅当第 个字母在这个单词中,其中 。
用数组 记录每个单词的位掩码表示。计算数组 之后,判断第 个单词和第 个单词是否有公共字母可以通过判断 是否等于 实现,当且仅当 时第 个单词和第
class Solution {
public:
int maxProduct(vector<string>& words) {
int res = 0;
vector<int> mask(words.size());
for(int i = 0; i < words.size(); i++){
string word = words[i];
for(int j = 0; j < word.size(); j++){
mask[i] |= 1 << (word[j] - 'a');
}
}
for(int i = 0; i < words.size(); i++){
for(int j = i + 1; j < words.size(); j++){
if((mask[i] & mask[j]) == 0){
res = max(res, (int)(words[i].size() * words[j].size()));
}
}
}
return res;
}
};
方法二:位运算优化
方法一需要对数组 中的每个单词计算位掩码,如果数组 中存在由相同的字母组成的不同单词,则会造成不必要的重复计算。例如单词 和 包含的字母相同,只是字母的出现次数和单词长度不同,因此这两个单词的位掩码表示也相同。由于判断两个单词是否有公共字母是通过判断两个单词的位掩码的按位与运算实现,因此在位掩码相同的情况下,单词的长度不会影响是否有公共字母,当两个位掩码的按位与运算等于
class Solution {
public:
int maxProduct(vector<string>& words) {
unordered_map<int, int> mp;
for(int i = 0; i < words.size(); i++){
int mask = 0;
string word = words[i];
int wordLength = word.size();
for(int j = 0; j < wordLength; j++){
mask |= 1 << (word[j] - 'a');
}
if(mp.count(mask)){
if(wordLength > mp[mask]){
mp[mask] = wordLength;
}
}else{
mp[mask] = wordLength;
}
}
int res = 0;
for(auto [mask1, _] : mp){
int wordLength1 = mp[mask1];
for(auto [mask2, _] : mp){
if((mask1 & mask2) == 0){
int wordLength2 = mp[mask2];
res = max(res, wordLength1 * wordLength2);
}
}
}
return res;
}
};
剑指 Offer II 010. 和为 k 的子数组
给定一个整数数组和一个整数 k
,请找到该数组中和为 k
的连续子数组的个数。
示例 1:
输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况
示例 2:
输入:nums = [1,2,3], k = 3
输出: 2
解析:
方法:前缀和 + 哈希表优化
先得到前缀和数组,然后以前缀和数组中的每一个值为右端点,在哈希表中查找满足条件的左端点(preSum[end+1] - preSum[start] = k ==> preSum[start] = preSum[end+1] - k),更新答案,并将preSum[end+1]插入哈希表中。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int n = nums.size();
vector<int> preSum(n+1);
preSum[0] = 0;
for(int i = 1; i <= n; i++){
preSum[i] = preSum[i-1] + nums[i-1];
}
int res = 0;
unordered_map<int, int> mp;//数字 -> 出现次数
mp[0] = 1;
for(int end = 0; end < n; end++) {
int target = preSum[end+1] - k;
if(mp.count(target)){
res += mp[target];
}
mp[preSum[end+1]]++;
}
return res;
}
};
剑指 Offer II 011. 0 和 1 个数相同的子数组
给定一个二进制数组 nums
, 找到含有相同数量的 0
和 1
的最长连续子数组,并返回该子数组的长度。
示例 1:
输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
方法一:前缀和 + 哈希表
由于「0 和 1 的数量相同」等价于「1 的数量减去 0 的数量等于 0」,可以将数组中的 0 视作 -1,则原问题转换成「求最长的连续子数组,其元素和为 0」。
- 将0替换为-1,遍历求前缀和
- 在遍历过程中,把前缀和和下标进行映射(多个相同前缀和时只记录最小的下标)
- 每遍历一个元素,就用「当前前缀和」去前面已经统计的前缀和中找到一个使得两者之间区间为0的,并计算这个区间长度
class Solution {
public:
int findMaxLength(vector<int>& nums) {
int maxLength = 0;
int n = nums.size();
unordered_map<int, int>mp;
mp[0] = -1; //规定空的前缀的结束下标为-1,由于空的前缀的元素和为0,因此在遍历之前,首先在哈希表中存入键值对(0,-1)
int cnt = 0;// 动态统计当前前缀和
for(int i = 0; i < n; i++){
int num = nums[i];
if(num == 1){
cnt++;
}else{
cnt--;
}
if(mp.count(cnt)){
int prevIndex = mp[cnt];
maxLength = max(maxLength, i - prevIndex);
}else{
mp[cnt] = i;
}
}
return maxLength;
}
};
剑指 Offer II 013. 二维子矩阵的和
给定一个二维矩阵 matrix
,以下类型的多个请求:
- 计算其子矩形范围内元素的总和,该子矩阵的左上角为
(row1, col1)
,右下角为(row2, col2)
。
实现 NumMatrix
类:
-
NumMatrix(int[][] matrix)
给定整数矩阵matrix
进行初始化 -
int sumRegion(int row1, int col1, int row2, int col2)
返回左上角(row1, col1)
、右下角(row2, col2)
的子矩阵的元素总和。
示例 1:
输入:
["NumMatrix","sumRegion","sumRegion","sumRegion"]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],[2,1,4,3],[1,1,2,2],[1,2,2,4]]
输出:
[null, 8, 11, 12]
解释:
NumMatrix numMatrix = new NumMatrix([[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (红色矩形框的元素总和)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (绿色矩形框的元素总和)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (蓝色矩形框的元素总和)
解析:
二维数组前缀和
class NumMatrix {
public:
vector<vector<int>> preSum;
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
preSum.resize(m + 1);
for(int i = 0; i <= m; i++){
preSum[i].resize(n + 1);
}
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] - preSum[i - 1][j - 1] + matrix[i - 1][j - 1];
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
return preSum[row2 + 1][col2 + 1] - preSum[row1][col2 + 1] - preSum[row2 + 1][col1] + preSum[row1][col1];
}
};
剑指 Offer II 024. 反转链表
给定单链表的头节点 head
,请反转链表,并返回反转后的链表的头节点。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
解析:
方法一:递归
/**
* 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* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr){
return head;
}
ListNode* newHead = reverseList(head->next);
ListNode* tmp = newHead;
while(tmp->next){
tmp = tmp->next;
}
tmp->next = head;
head->next = nullptr;
return newHead;
}
};
方法二:迭代
使用头插法
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* dummy = new ListNode();
while(head != nullptr){
ListNode* tmp = head->next;
head->next = dummy->next;
dummy->next = head;
head = tmp;
}
return dummy->next;
}
};
剑指 Offer II 026. 重排链表
给定一个单链表 L
的头节点 head
,单链表 L
表示为:
L0 → L1 → … → Ln-1 → Ln
请将其重新排列后变为:
L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
输入: head = [1,2,3,4]
输出: [1,4,2,3]
示例 2:
输入: head = [1,2,3,4,5]
输出: [1,5,2,4,3]
解析:
方法一:线性表
因为链表不支持下标访问,所以我们无法随机访问链表中任意位置的元素。
因此比较容易想到的一个方法是,我们利用线性表存储该链表,然后利用线性表可以下标访问的特点,直接按顺序访问指定元素,重建该链表即可。
class Solution {
public:
void reorderList(ListNode* head) {
ListNode* tmp = head;
stack<ListNode*> st;
while(tmp){
st.push(tmp);
tmp = tmp->next;
}
int n = st.size() / 2;
ListNode* cur = head;
while(n--){
tmp = st.top();
st.pop();
tmp->next = cur->next;
cur->next = tmp;
cur = tmp->next;
}
cur->next = NULL;
}
};
方法二:寻找链表中点 + 链表逆序 + 合并链表
class Solution {
public:
void reorderList(ListNode* head) {
ListNode* slow = head, *fast = head;
while(fast && fast->next){
fast = fast->next->next;
slow = slow->next;
}
ListNode* mid = reverseList(slow);
ListNode* dummy = new ListNode();
dummy->next = head;
int k = 1;
while(head && mid){
if(k % 2){
dummy->next = head;
dummy = head;
head = head->next;
}else{
dummy->next = mid;
dummy = mid;
mid = mid->next;
}
k++;
}
}
ListNode* reverseList(ListNode* head)
{
ListNode* new_head = NULL;//指向新链表头节点的指针
while (head)
{
ListNode* next = head->next;//备份head->next
//反转
head->next = new_head;//更新head->next
//迭代
new_head = head;//移动new_head
head = next;//遍历链表
}
return new_head;//返回新链表头节点
}
};
剑指 Offer II 028. 展平多级双向链表
多级双向链表中,除了指向下一个节点和前一个节点指针之外,它还有一个子链表指针,可能指向单独的双向链表。这些子列表也可能会有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。
给定位于列表第一级的头节点,请扁平化列表,即将这样的多级双向链表展平成普通的双向链表,使所有结点出现在单级双链表中。
示例 1:
输入:head = [1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]
输出:[1,2,3,7,8,11,12,9,10,4,5,6]
解释:输入的多级列表如下图所示:
扁平化后的链表如下图:
解析:
方法一:深度优先搜索
当遍历到某个节点 时,如果它的 成员不为空,那么需要将 指向的链表结构进行扁平化,并且插入 与
因此,在遇到 成员不为空的节点时,就要先去处理 指向的链表结构,这就是一个「深度优先搜索」的过程。当完成了对 指向的链表结构的扁平化之后,就可以「回溯」到
为了能够将扁平化的链表插入 与 的下一个节点之间,需要知道扁平化的链表的最后一个节点 ,随后进行如下的三步操作:
- 将 与 的下一个节点
- 将 与
- 将 与
这样一来,就可以将扁平化的链表成功地插入。
class Solution {
public:
Node* flatten(Node* head) {
if(head == nullptr){
return head;
}
// 找到第一个有孩子节点的节点tmp(3)
Node* tmp = head;
while(tmp && !tmp->child){
tmp = tmp->next;
}
if(tmp == nullptr){
return head;
}
// 利用函数定义将tmp下的子链表进行展开,得到头pivotH(7)和尾pivotT(10)
Node* pivotH = flatten(tmp->child);
Node* pivotT = pivotH;
while(pivotT && pivotT->next){
pivotT = pivotT->next;
}
// 连接,3->next = 7, 7->prev = 3, 10->next = 4, 4->prev = 10
if(pivotT && tmp){
pivotT->next = tmp->next;
if(tmp->next){
tmp->next->prev = pivotT;
}
tmp->next = pivotH;
tmp->child = nullptr;
pivotH->prev = tmp;
}
return head;
}
};
剑指 Offer II 029. 排序的循环链表
给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素 insertVal
,使这个列表仍然是循环升序的。
给定的可以是这个列表中任意一个顶点的指针,并不一定是这个列表中最小元素的指针。
如果有多个满足条件的插入位置,可以选择任意一个位置插入新的值,插入后整个列表仍然保持有序。
如果列表为空(给定的节点是 null
),需要创建一个循环有序列表并返回这个节点。否则。请返回原先给定的节点。
示例 1:
输入:head = [3,4,1], insertVal = 2
输出:[3,4,1,2]
解释:在上图中,有一个包含三个元素的循环有序列表,你获得值为 3 的节点的指针,我们需要向表中插入元素 2 。新插入的节点应该在 1 和 3 之间,插入之后,整个列表如上图所示,最后返回节点 3 。
解析:
class Solution {
public:
Node* insert(Node* head, int insertVal) {
Node *node = new Node(insertVal);
if (head == nullptr) {
node->next = node;
return node;
}
if (head->next == head) {
head->next = node;
node->next = head;
return head;
}
Node *curr = head, *next = head->next;
while (next != head) {
if (insertVal >= curr->val && insertVal <= next->val) {
break;
}
if (curr->val > next->val) {
if (insertVal > curr->val || insertVal < next->val) {
break;
}
}
curr = curr->next;
next = next->next;
}
curr->next = node;
node->next = next;
return head;
}
};
剑指 Offer II 036. 后缀表达式
根据 逆波兰表示法,求该后缀表达式的计算结果。
有效的算符包括 +
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
- 整数除法只保留整数部分。
- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
逆波兰表达式:
逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。
- 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
- 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
逆波兰表达式主要有以下两个优点:
- 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
- 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
示例 1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
解析:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
//遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。
stack<int> st;
for(int i = 0; i < tokens.size(); i++){
if(tokens[i] != "+" && tokens[i] != "-" && tokens[i] != "*" && tokens[i] != "/"){
st.push(atoi(tokens[i].c_str()));
}else{
int t1 = st.top(); st.pop();
int t2 = st.top(); st.pop();
if(tokens[i] == "+"){
st.push(t2 + t1);
}else if(tokens[i] == "-"){
st.push(t2 - t1);
}else if(tokens[i] == "*"){
st.push(t2 * t1);
}else if(tokens[i] == "/"){
st.push(t2 / t1);
}
}
}
return st.top();
}
};
剑指 Offer II 047. 二叉树剪枝
给定一个二叉树 根节点 root
,树的每个节点的值要么是 0
,要么是 1
。请剪除该二叉树中所有节点的值为 0
的子树。
节点 node
的子树为 node
本身,以及所有 node
的后代。
示例 1:
输入: [1,null,0,0,1]
输出: [1,null,0,null,1]
解释:
只有红色节点满足条件“所有不包含 1 的子树”。
右图为返回的答案。
示例 2:
输入: [1,0,1,0,0,0,1]
输出: [1,null,1,null,1]
解释:
解析:
方法:递归
树相关的题目首先考虑用递归解决。
- 首先确定边界条件,当输入为空时,即可返回空。
- 然后对左子树和右子树分别递归进行
- 递归完成后,当这三个条件:左子树为空,右子树为空,当前节点的值为 ,同时满足时,才表示以当前节点为根的原二叉树的所有节点都为 ,需要将这棵子树移除,返回空。有任一条件不满足时,当前节点不应该移除,返回当前节点。
class Solution {
public:
TreeNode* pruneTree(TreeNode* root) {
if (!root) {
return nullptr;
}
root->left = pruneTree(root->left);
root->right = pruneTree(root->right);
if (!root->left && !root->right && !root->val) {
return nullptr;
}
return root;
}
};
剑指 Offer II 050. 向下的路径节点之和
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
解析:
方法:深搜 + 前缀和 + 哈希表
class Solution {
public:
int res = 0, trackSum = 0;
unordered_map<int, int> mp;
int pathSum(TreeNode* root, int targetSum) {
mp[0] = 1; // 因为空路径不经过任何节点,因此它的前缀和为 0
backtrack(root, targetSum);
return res;
}
void backtrack(TreeNode* root, int targetSum){
if(root == nullptr){
return;
}
trackSum += root->val;
if(mp.count(trackSum - targetSum)){
res += mp[trackSum - targetSum];
}
mp[trackSum]++;
backtrack(root->left, targetSum);
backtrack(root->right, targetSum);
mp[trackSum]--;
trackSum -= root->val;
}
};
剑指 Offer II 051. 节点之和最大的路径
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给定一个二叉树的根节点 root
,返回其 最大路径和,即所有路径上节点值之和的最大值。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
解析:
方法一:递归
class Solution {
public:
int res = INT_MIN;
int maxPathSum(TreeNode* root) {
process(root);
return res;
}
// 以root为起点的最大路径和
int process(TreeNode* root){
if(root == nullptr){
return 0;
}
int left = max(0, process(root->left));
int right = max(0, process(root->right));
res = max(res, root->val + left + right);
return max(left, right) + root->val;
}
};
方法二:遍历
遍历每一个节点,计算以当前节点为根节点的最大路径和(当前节点的值+其左子树的最大路径+其右子树的最大路径)
class Solution {
public:
int res = 0, trackSum = 0;
int maxPathSum(TreeNode* root) {
int ans = INT_MIN;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
int sz = q.size();
for(int i = 0; i < sz; i++){
TreeNode* cur = q.front();
backtrack(cur->left);
int tmp = cur->val + max(0, res);
res = 0;
backtrack(cur->right);
tmp += max(0, res);
res = 0;
ans = max(ans, tmp);
q.pop();
if(cur->left) q.push(cur->left);
if(cur->right) q.push(cur->right);
}
}
return ans;
}
void backtrack(TreeNode* root){
if(root == nullptr){
return;
}
trackSum += root->val;
res = max(res, trackSum);
backtrack(root->left);
backtrack(root->right);
trackSum -= root->val;
}
};
剑指 Offer II 053. 二叉搜索树中的中序后继
给定一棵二叉搜索树和其中的一个节点 p
,找到该节点在树中的中序后继。如果节点没有中序后继,请返回 null
。
节点 p
的后继是值比 p.val
大的节点中键值最小的节点,即按中序遍历的顺序节点 p
的下一个节点。
示例 1:
输入:root = [2,1,3], p = 1
输出:2
解释:这里 1 的中序后继是 2。请注意 p 和返回值都应是 TreeNode 类型。
示例 2:
输入:root = [5,3,6,2,4,null,null,1], p = 6
输出:null
解释:因为给出的节点没有中序后继,所以答案就返回 null 了。
解析:
方法一:中序遍历之迭代
由于只需要找到节点 p 的中序后继,因此不需要维护完整的中序遍历序列,只需要在中序遍历的过程中维护上一个访问的节点和当前访问的节点。
- 如果上一个访问的节点是节点 p,则当前访问的节点即为节点 p 的中序后继。
- 如果节点 p 是最后被访问的节点,则不存在节点 p 的中序后继,返回 null。
class Solution {
public:
TreeNode* pre = NULL;
TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
stack<TreeNode*> st;
TreeNode* cur = root;
while(cur != NULL || !st.empty()){
if(cur != NULL){
st.push(cur);
cur = cur->left;
}else{
cur = st.top();
st.pop();
if(pre == p){
return cur;
}
pre = cur;
cur = cur->right;
}
}
return NULL;
}
};
方法二:中序遍历之递归
class Solution {
public:
TreeNode* pre = NULL;
TreeNode* tmp = NULL;
TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p) {
return inOrder(root, p);
}
TreeNode* inOrder(TreeNode* root, TreeNode* p){
if(root == NULL){
return root;
}
inOrder(root->left, p);
if(pre == p){
tmp = root;
}
pre = root;
inOrder(root->right, p);
return tmp;
}
};
剑指 Offer II 070. 排序数组中只出现一次的数字
给定一个只包含整数的有序数组 nums
,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字。
你设计的解决方案必须满足 O(log n)
时间复杂度和 O(1)
空间复杂度。
示例 1:
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: nums = [3,3,7,7,10,11,11]
输出: 10
解析:
二分法
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int start = 0, end = nums.size() - 1;
while(start < end){
int mid = start + (end - start) / 2;
if((mid - start + 1) % 2){ // [start, mid]中有奇数个元素
if(nums[mid] != nums[mid + 1]){
end = mid;
}else{
start = mid + 2;
}
}else{ // [start, mid]中有偶数个元素
if(nums[mid] == nums[mid + 1]){
end = mid - 1;
}else{
start = mid + 1;
}
}
}
return nums[start];
}
};
剑指 Offer II 081. 允许重复选择元素的组合
给定一个无重复元素的正整数数组 candidates
和一个正整数 target
,找出 candidates
中所有可以使数字和为目标数 target
的唯一组合。
candidates
中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的唯一组合数少于 150
个。
示例 1:
输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]
解析:
DFS
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
int trackSum = 0;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtrack(candidates, target, 0);
return res;
}
void backtrack(vector<int>& candidates, int target, int start){
int n = candidates.size();
if(trackSum == target){
res.push_back(track);
return;
}
if(trackSum > target){
return;
}
for(int i = start; i < n; i++){
if(candidates[i] > target){
continue;
}
trackSum += candidates[i];
track.push_back(candidates[i]);
backtrack(candidates, target, i);
track.pop_back();
trackSum -= candidates[i];
}
}
};
剑指 Offer II 082. 含有重复元素集合的组合
给定一个可能有重复数字的整数数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用一次,解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
解析:
DFS
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
int trackSum;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0);
return res;
}
void backtrack(vector<int>& candidates, int target, int start){
if(trackSum == target){
res.push_back(track);
return;
}
if(trackSum > target){
return;
}
for(int i = start; i < candidates.size(); i++){
if(i > start && candidates[i] == candidates[i - 1]){
continue;
}
trackSum += candidates[i];
track.push_back(candidates[i]);
backtrack(candidates, target, i + 1);
track.pop_back();
trackSum -= candidates[i];
}
}
};
剑指 Offer II 083. 没有重复元素集合的全排列
给定一个不含重复数字的整数数组 nums
,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
解析:
DFS
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size());
backtrack(nums, used);
return res;
}
void backtrack(vector<int>& nums, vector<bool>& used){
if(track.size() == nums.size()){
res.push_back(track);
return;
}
for(int i = 0; i < nums.size(); i++){
if(used[i]){
continue;
}
track.push_back(nums[i]);
used[i] = true;
backtrack(nums, used);
used[i] = false;
track.pop_back();
}
}
};
剑指 Offer II 084. 含有重复元素集合的全排列
给定一个可包含重复数字的整数集合 nums
,按任意顺序 返回它所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
解析:
DFS
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<bool> used(nums.size());
backtrack(nums, used);
return res;
}
void backtrack(vector<int>& nums, vector<bool>& used){
if(track.size() == nums.size()){
res.push_back(track);
return;
}
for(int i = 0; i < nums.size(); i++){
if(used[i]){
continue;
}
// 剪枝逻辑,固定相同的元素在排列中的相对位置
if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
continue;
}
track.push_back(nums[i]);
used[i] = true;
backtrack(nums, used);
used[i] = false;
track.pop_back();
}
}
};
剑指 Offer II 092. 翻转字符
如果一个由 '0'
和 '1'
组成的字符串,是以一些 '0'
(可能没有 '0'
)后面跟着一些 '1'
(也可能没有 '1'
)的形式组成的,那么该字符串是 单调递增 的。
我们给出一个由字符 '0'
和 '1'
组成的字符串 s,我们可以将任何 '0'
翻转为 '1'
或者将 '1'
翻转为 '0'
。
返回使 s 单调递增 的最小翻转次数。
示例 1:
输入:s = "00110"
输出:1
解释:我们翻转最后一位得到 00111.
解析:
同leetcode926题
方法一:前缀和 + 枚举
利用 s 只存在 0 和 1 两种数值,我们知道最后的目标序列形如 000...000
、000...111
或 111...111
的形式。
因此我们可以枚举目标序列的 0 和 1 分割点位置 idx(分割点是 0 是 1 都可以,不消耗改变次数)。
于是问题转换为:分割点 idx 左边有多少个 1(目标序列中分割点左边均为 0,因此 1 的个数为左边的改变次数),分割点 idx 的右边有多少个 0(目标序列中分割点右边均为 1,因此 0 的个数为右边的改变次数),两者之和即是分割点为 idx 时的总变化次数,所有 idx 的总变化次数最小值即是答案。
而求解某个点左边或者右边有多少 1 和 0 可通过「前缀和」进行优化。
class Solution {
public:
int minFlipsMonoIncr(string s) {
int n = s.size();
int ans = n;
vector<int> preSum(n + 1, 0);
for(int i = 1; i <= n; i++){
preSum[i] = preSum[i - 1] + s[i - 1] - '0';
}
// 分割点本身不用计算在内
for(int i = 0; i < n; i++){
int left = preSum[i];
int right = (n - 1 - i) - (preSum[n] - preSum[i + 1]);
ans = min(ans, left + right);
}
return ans;
}
};
方法二:LIS 问题二分查找
将原题进行等价转换:令 s 长度为 n,原问题等价于在 s 中找到最长不下降子序列,设其长度为 ans,那么对应的 n - ans 即是答案。 该做法不依赖于数组只有 0 和 1 的前提条件,适用范围更广。
由于数据范围为 1e5,因此需要使用 LIS 问题的二分求解方式。
首先,给你⼀排扑克牌,我们像遍历数组那样从左到右⼀张⼀张处理这些扑克牌,最终要把这些牌分成若干堆。
处理这些扑克牌要遵循以下规则: 只能把点数小的牌压到点数比它大的牌上。如果当前牌没有可以放置的堆,则新建⼀个堆,把这张牌放进去。如果当前牌有多个堆可供选择, 则选择最左边的堆放置。
每次处理⼀张扑克牌不是要找⼀个合适的牌堆顶来放吗,牌堆顶的牌不是有序吗,这就能用到⼆分查找了:用⼆分查找来搜索当前牌应放置的位置。
class Solution {
public:
int minFlipsMonoIncr(string s) {
return s.size() - lengthOfLIS(s);
}
int lengthOfLIS(string &s){
int n = s.length();
vector<int> top(n);
top[0] = s[0] - '0';
int piles = 1;
for(int i = 1; i < n; i++){
int poker = s[i] - '0';
int left = 0, right = piles - 1;
while(left + 1 < right){
int mid = left + (right - left) / 2;
if(top[mid] <= poker){
left = mid;
}else{
right = mid;
}
}
if(top[left] > poker){
top[left] = poker;
}else if(top[right] > poker){
top[right] = poker;
}else{
top[piles++] = poker;
}
}
return piles;
}
};
方法三:动态规划
(一)确定状态
假设字符串 的长度是 ,对于 ,用 和 分别表示下标 处的字符为 和 的情况下使得
(二)转移方程
如果下标 处的字符是 ,则只有当下标 处的字符是 时才符合单调递增;如果下标 处的字符是 ,则下标 处的字符是 或 都符合单调递增,此时为了将翻转次数最小化,应分别考虑下标 处的字符是 和 的情况下需要的翻转次数,取两者的最小值。
在计算 和 时,还需要根据 的值决定下标 处的字符是否需要翻转,因此可以得到如下状态转移方程:
dp[i][0] = dp[i - 1][0] + (s[i] == 1)
dp[i][1] = min(dp[i - 1][0], dp[i - 1][1]) + (s[i] == 0)
(三)初始条件和边界情况
dp[0][0] = s[0] == 1:只有一个元素且要求其为0时,则当其为1时需要翻转
dp[0][1] = s[0] == 0:只有一个元素且要求其为1时,则当其为0时需要翻转
(四)计算顺序
从前往后计算,答案为 min(dp[n - 1][0], dp[n - 1][1])
class Solution {
public:
int minFlipsMonoIncr(string s) {
int n = s.size();
vector<vector<int>> f(2, vector<int> (2));
//f[i][0]表示在s[0...i],最后一个元素为0的最小翻转次数;
//f[i][1]表示在s[0...i],最后一个元素为1的最小翻转次数
f[0][0] = s[0] == '1';
f[0][1] = s[0] == '0';
for(int i = 1; i < n; i++){
f[i % 2][0] = f[(i - 1) % 2][0] + (s[i] == '1');
f[i % 2][1] = min(f[(i - 1) % 2][0], f[(i - 1) % 2][1]) + (s[i] == '0');
}
return min(f[(n - 1) % 2][0], f[(n - 1) % 2][1]);
}
};