算法提升(2):递归到动态规划、哈希表应用、magic操作、括号匹配序列及最长合法子序列、单调栈应用、树形dp、从右上角往左下角比对移动的模型
题目1
给定一个非负整数n,代表二叉树的节点个数。返回能形成多少种不同的二叉树结构
递归
假设现在有m个节点,左子树的节点数从0开始到m-1,右子树的节点数从m-1开始到0,两边的种类数相乘(左右子树的节点数分别为0时不相乘,只算另一侧不为0的)然后累加,最后的结果就是m个节点时的种类数
int process(int n);
int binaryTree(int n)
{
return process(n);
}
int process(int n)
{
if (n == 0)
{
return 0;
}
if (n == 1)
{
return 1;
}
if (n == 2)
{
return 2;
}
int res = 0;
for (int i = 0; i <= n - 1; i++)
{
int leftNum = process(i);
int rightNum = process(n - i - 1);
if (i == 0)
{
res += rightNum;
}
else if(n - i - 1 == 0)
{
res += leftNum;
}
else
{
res += leftNum * rightNum;
}
}
return res;
}
动态规划
int binaryTreeNums2(int n)
{
if (n == 0)
{
return 0;
}
if (n == 1)
{
return 1;
}
vector<int> dp(n + 1);
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i < n + 1; i++)
{
for (int j = 0; j < i; j++)
{
if (j == 0)
{
dp[i] += dp[i - j - 1];
}
else if (i - j - 1 == 0)
{
dp[i] += dp[j];
}
else
{
dp[i] += dp[j] * dp[i - j - 1];
}
}
}
return dp[n];
}
题目2
一个完整的括号字符串定义规则如下:
- 空字符串是完整的。
- 如果s是完整的字符串,那么(s)也是完整的。
- 如果s和t是完整的字符串,将它们连接起来形成的st也是完整的。
例如,"(()())",""和"(())()"是完整的括号字符串,"())(", "()(”和")"是不完整的括号字符串。
牛牛有一个括号字符串s,现在需要在其中任意位置尽量少地添加括号,将其转化为一个完整的括号字符串。请问牛牛至少需要添加多少个括号。
思路
准备一个count指针,初始值0,准备一个变量ans,初始值为0。遍历字符串,count遇到"("就+1,遇到")"就-1。我们考虑这样一种情况,如果整体上多余的是左括号,那么只要在这个括号后面的任意位置加上一个右括号就可以补充完整,所以不管是什么位置的右括号,只要它前面还有没配对的左括号,就可以完成配对,所以每当count到-1时就代表有一个多余的右括号,这时ans++,记录这个多余的右括号,然后把count回调到0,回调是为了不让多余的右括号干扰多余左括号的计数。当字符串遍历完后,count记录多余的左括号,ans记录多余的右括号,ans+count的结果就是答案。
int completeParentheses(string s)
{
int count = 0;
int ans = 0;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == '(')
{
count++;
}
else
{
if (count == 0) //遇到右括号且count为0时,count不变,ans++,用count不变省略掉变成-1再回调的操作
{
ans++;
}
else
{
count--;
}
}
}
return ans + count;
}
题目3
给定一个数组arr,求差值为k的去重数字对。
思路
准备一个哈希表(c++里可以用set,既可以排序也可以去除数组中的重复值),数组里所有的值都存进去,遍历哈希表,查找哈希表中有没有当前遍历到的值+k的值,如果有就记录下来,直到哈希表遍历完。
vector<vector<int>> kDifference(vector<int> arr, int k)
{
set<int> arrSet;
vector<vector<int>> ans;
for (int i = 0; i < arr.size(); i++)
{
arrSet.insert(arr[i]);
}
for (auto it : arrSet)
{
if (arrSet.count(it + k) == 1)
{
vector<int> fitPair(2);
fitPair[0] = it;
fitPair[1] = it + k;
ans.push_back(fitPair);
}
}
return ans;
}
题目4
给一个包含n个整数元素的集合a,一个包含m个整数元素的集合b。
定义magic操作为,从一个集合中取出一个元素,放到另一个集合里,且操作过后每个集合的平均值都大大于于操作前。
注意以下两点:
- 不可以把一个集合的元素取空,这样就没有平均值了
- 值为x的元素从集合b取出放入集合a,但集合a中已经有值为x的元素,则a的平均值不变(因为集合元素不会重复),b的平均值可能会改变(因为x被取出了)
问最多可以进行多少次magic操作?
思路
只能从平均值大的集合里拿数放入平均值小的集合。假设平均值大的集合为a集合,平均值是bigAver;小的那个集合为b集合,平均值是smallAver。要从a集合里拿大于smallAver小于bigAver的值。那符合要求的多个值怎么选,选最小的,这样可以保证bigAver增加最大的幅度,smallAver增加最小的幅度,保证bigAver与smallAver的差值最大,这样才能更多地进行magic操作。直到bigAver等于smallAver时,就不能进行magic操作了。
double avg(double sum, int length);
int magic(vector<int> arr1, vector<int> arr2)
{
double sum1 = 0;
for (int i = 0; i < arr1.size(); i++)
{
sum1 += arr1[i];
}
double sum2 = 0;
for (int i = 0; i < arr2.size(); i++)
{
sum2 += arr2[i];
}
if (avg(sum1, arr1.size()) == avg(sum2, arr2.size()))
{
return 0;
}
vector<int> arrMore;
double sumMore;
int moreSize;
vector<int> arrLess;
double sumLess;
int lessSize;
if (avg(sum1, arr1.size()) > avg(sum2, arr2.size()))
{
arrMore = arr1;
sumMore = sum1;
moreSize = arr1.size();
arrLess = arr2;
sumLess = sum2;
lessSize = arr2.size();
}
else
{
arrMore = arr2;
sumMore = sum2;
moreSize = arr2.size();
arrLess = arr1;
sumLess = sum1;
lessSize = arr1.size();
}
sort(arrMore.begin(), arrMore.end());
unordered_set<int> hashSet;
for (int i = 0; i < arrLess.size(); i++)
{
hashSet.insert(arrLess[i]);
}
int ans = 0;
for (int i = 0; i < arrMore.size(); i++)
{
double cur = arrMore[i];
if (cur > avg(sumMore, moreSize) && cur < avg(sumLess, lessSize) && hashSet.count(arrMore[i]) == 0)
{
sumMore -= cur;
moreSize--;
sumLess += cur;
lessSize++;
hashSet.insert(arrMore[i]);
ans++;
}
}
return ans;
}
double avg(double sum, int length)
{
return sum / length;
}
题目5:
一个合法的括号匹配序列有以下定义:
- 空串""是一个合法的括号匹配序列
- 如果"X"和"Y"都是合法的括号匹配序列,"XY"也是一个合法的括号匹配序列
- 如果"X"是一个合法的括号匹配序列,那么" (X) "也是一个合法的括号匹配序列
- 每个合法的括号序列都可以由以上规则生成。
例如:
"","()","()()", "(())"都是合法的括号序列
对于一个合法的括号序列我们又有以下定义它的深度:
- 空串""的深度是0
- 如果字符串"X"的深度是x,字符串"Y"的深度是y,那么字符串"XY"的深度为max(x,y)
- 如果"X"的深度是x,那么字符串" (X) "的深度是x+1
例如:
"() () ()"的深度是1,"((()))"的深度是3。牛牛现在给你一个合法的括号序列,需要你计算出其深度。
思路
准备一个count指针,初始值0。遍历字符串,count遇到"("就+1,遇到")"就-1。count在变化时的最大值就是深度。
int maxDepth(string s)
{
int count = 0;
int ans = 0;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == '(')
{
count++;
}
else
{
count--;
}
ans = ans < count ? count : ans;
}
return ans;
}
题目6
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确,即前面题目提到的合法的,且连续)括号子串的长度。
思路
必须连续的子串或子数组的情况,就求以每个字符或数结尾的情况下答案是多少
申请一个辅助数组,长度与括号字符串一致,存放着以当前位置在括号字符串对应位置的字符结尾的情况下有效长度是多少。当前来到位置i,分情况讨论
- 如果i位置是左括号,那么以i位置结尾无论如何都不合法,所以长度是0
- 如果i位置是右括号,看i-1位置存放的值是多少,假设是m,则从i-1开始往前移动m位,找到以i-1位置结尾的最长有效字串的开头的前一个位置,看它是否是左括号(因为要与i位置的右括号匹配)。如果不是,那么i位置就是0。如果是,则i位置是2+(i-1位置的最长有效字串长度)+(i-1-m位置的最长有效字串长度)。
int longestValidParentheses(string s)
{
vector<int> dp(s.size());
int max = 0;
int pre = 0;
for (int i = 0; i < dp.size(); i++)
{
if (s[i] == ')' && i != 0)
{
pre = i - 1 - dp[i - 1]; //以i-1位置结尾的最长有效字串的开头的前一个位置
if (pre >= 0 && s[pre] == '(') //如果不越界且是左括号
{
dp[i] = 2 + dp[i - 1] + (pre > 0 ? dp[pre - 1] : 0); //i-1-m位置不越界就加上以它结尾的最长有效字串长度,越界就替换成0
}
}
max = dp[i] > max ? dp[i] : max;
}
return max;
}
题目7
请编写一个程序,对一个栈里的整型数据,按升序进行排序(即排序前栈里的数据是无序的,排序后最大元素位于栈顶),要求最多只能使用一个额外的栈存放临时数据,但不得将元素复制到别的数据结构中。
思路
维护一个栈底到栈顶从大到小的单调栈,每次从原来的栈弹出数据进单调栈,由于进单调栈弹出的数据还进原来的栈,直到原栈为空,全部数据都进了单调栈,把单调栈栈底到栈顶从大到小的数据依次弹出进原来的栈就排序好了。
//leetcode第32题写法
class SortedStack {
stack<int> s;
stack<int> monotStack;
public:
SortedStack() {
}
void push(int val) {
while (!s.empty() && s.top() < val)
{
int temp = s.top();
s.pop();
monotStack.push(temp);
}
s.push(val);
while(!monotStack.empty())
{
int popBackToS = monotStack.top();
monotStack.pop();
s.push(popBackToS);
}
}
void pop() {
if(!s.empty())
{
s.pop();
}
}
int peek() {
if(!s.empty())
{
return s.top();
}
return -1;
}
bool isEmpty() {
return s.empty();
}
};
//函数写法,把传入的栈s按升序进行排序
void sortStack(stack<int> &s)
{
if (!s.empty())
{
stack<int> monotStack; //单调栈
while (!s.empty())
{
while (monotStack.empty() || monotStack.top() >= s.top())
{
int temp = s.top();
s.pop();
monotStack.push(temp);
if (s.empty())
{
break;
}
}
if (!s.empty()) //如果此时栈s不为空,代表此时的栈顶元素不能进入单调栈
{
int temp = s.top(); //temp捕获栈顶元素
s.pop();
while (!monotStack.empty() && monotStack.top() < temp) //从单调栈中弹回栈s,直到temp可以进单调栈
{
int popBackToS = monotStack.top();
monotStack.pop();
s.push(popBackToS);
}
monotStack.push(temp);
}
}
while (!monotStack.empty()) //在单调栈里排完序后再弹回栈s
{
int popBackToS = monotStack.top();
monotStack.pop();
s.push(popBackToS);
}
}
}
题目8
将给定的数转换为字符串,原则如下: 1对应a, 2对应b,... 26对应z,例如12258可以转换为" abbeh","aveh", " abyh","Ibeh" and "Iyh", 个数为5,编写一个函数,给出可以转换的不同字符串的个数。
思路
从左到右的尝试模型,在“算法学习(16):暴力递归”中已经讲过这一题了,这里再写一遍,相比之前简化思路,来到i时,在i+1不越界的情况下看i与i+1字符组成的数字是否超过26,没超过就多调用process(arr,index+2),代码可以少很多if判断
暴力递归代码
int process2(string arr, int index);
int intToCharWays(string arr)
{
if (arr.empty())
{
return 0;
}
return process2(arr, 0);
}
int process2(string arr, int index)
{
if (index == arr.size())
{
return 1;
}
if (arr[index] == '0')
{
return 0;
}
int res = 0;
res += process2(arr, index + 1);
if (index < arr.size() - 1)
{
if (((arr[index] - '0') * 10 + (arr[index + 1] - '0')) <= 26)
{
res += process2(arr, index + 2);
}
}
return res;
}
动态规划代码
int intToCharWaysDP(string arr)
{
if (arr.empty())
{
return 0;
}
vector<int> dp(arr.size() + 1);
dp[arr.size()] = 1;
for (int i = arr.size() - 1; i >= 0; i--)
{
if (arr[i] != '0')
{
if (i< arr.size() - 1)
{
if (((arr[i] - '0') * 10 + (arr[i + 1] - '0')) <= 26)
{
dp[i] += dp[i + 2];
}
}
dp[i] += dp[i + 1];
}
}
return dp[0];
}
题目9
二叉树每个结点都有一个int型权值,给定一棵二叉树,要求计算出从根结点到叶结点的所有路径中,权值和最大的值为多少。
思路
树形dp,需要的信息是左子树的最大权值和,右子树的最大权值和
int process(TreeNode* head);
int maxWeight(TreeNode* head)
{
if (head == NULL)
{
return 0;
}
return process(head);
}
int process(TreeNode* head)
{
if (head == NULL)
{
return 0;
}
int leftMaxWeight = process(head->left);
int rightMaxWeight = process(head->right);
return head->val + max(leftMaxWeight, rightMaxWeight);
}
题目10
给定一个元素为非负整数的二维数组matrix,每行和每列都是从小到大有序的。再给定一个非负整数aim,请判断aim是否在matrix中。
思路
剑指offer原题,从右上角开始与aim比较,比aim大就往下走,直到找到或出界;比aim小就往左走,直到找到或出界
bool findAimInMatrix(vector<vector<int>> matrix, int aim)
{
if (matrix.size() == 0 || matrix[0].size() == 0 || aim < 0)
{
return false;
}
int row = matrix.size();
int col = matrix[0].size();
int i = 0;
int j = col - 1;
while (i < row && j >= 0)
{
if (matrix[i][j] == aim)
{
return true;
}
else if(matrix[i][j] > aim)
{
i++;
}
else
{
j++;
}
}
return false;
}
题目11
有一个由0和1组成的矩阵,每一行的左边是若干个连续的0,右边是若干个连续的1,返回1最多的行(数量一样就都返回)
思路
跟上一题思路一样类似。从右上角出发,先往左走一直走到最左边的1,记录本行的1的数量,走到最左边的1后往下走,如果下面一行的这一列是0,则证明下一行的1没有本行多,不统计下一行直接跳过到本列不是0的行,接着往左走到最左边的1,并统计数量,直到最后一行统计完毕,返回数量最多的行。
vector<int> maxOneRow(vector<vector<int>> matrix)
{
if (matrix.size() == 0 || matrix[0].size() == 0)
{
return {};
}
int row = matrix.size();
int col = matrix[0].size();
int i = 0;
int j = col - 1;
int val = 0; //记录当前最多的1的数量-1。目的是为了下面看看会不会变化
vector<int> ans;
while (i < row && j >= 0)
{
if (matrix[i][j] == 0)
{
i++;
}
else
{
int temp = val; //记录到本行时val的值
while (j - 1 >= 0 && matrix[i][j - 1] == 1)
{
j--;
val++;
}
if (temp != val && !ans.empty()) //如果val没有变化,则不清空ans
{
ans.clear();
}
ans.push_back(i);
i++;
}
}
return ans;
}
浙公网安备 33010602011771号