算法笔记(5)
题目1
一个子序列的消除规则如下:
- 在某一个子序列中,如果'1'的左边有'0',那么这两个字符 -> "01"可以消除
- 在某一个子序列中,如果'3'的左边有'2',那么这两个字符 -> "23"可以消除
- 当这个子序列的某个部分消除之后,认为其他字符会自动贴在一起,可以继续寻找消除的机会
比如,某个子序列"0231",先消除掉"23",那么剩下的字符贴在一起变成"01",继续消除就没有字符了
如果某个子序列通过最优良的方式,可以都消掉,那么这样的子序列叫做“全消子序列”
一个只由'0'、'1'、'2'、'3' 四种字符组成的字符串str,可以生成很多子序列,返回“全消子序列”的最大长度
字符串str长度 <= 200
思路
范围尝试模型。L到R上,返回全消子序列的最大长度。那这个函数就是f(L, R)的形式。两种可能,一种是不考虑L上的字符,一种是必须考虑L上的字符。
先说不考虑:不考虑的话就是返回f(L + 1, R)
考虑:首先看L位置上是否是'1'或者'3',如果是,因为必须考虑那么这种情况下全消子序列的长度就是0。如果L位置上是否是'0'或者'2',那么L+1 ~ R范围上有若干个跟L位置字符配对的字符,假设这些位置是i,每一个这样的字符都可能消掉,所以都调一次递归:f(L + 1, i - 1) + 2(L与i位置可以消掉,所以+2) + f(i + 1, R) ,拿到最大的结果,与第一种不考虑L位置对比,返回最大的。
代码:
int process(string &str, int L, int R)
{
if (L >= R)
{
return 0;
}
if (L + 1 == R)
{
return (str[L] == '0' && str[R] == '1') || (str[L] == '2' && str[R] == '3') ? 2 : 0;
}
int p1 = process(str, L + 1, R);
if (str[L] == '1' || str[L] == '3')
{
return p1;
}
char find = str[L] == '0' ? '1' : '3';
int p2 = 0;
for (int i = L + 1; i <= R; i++)
{
if (str[i] == find)
{
p2 = max(p2, process(str, L + 1, i - 1) + 2 + process(str, i + 1, R));
}
}
return max(p1, p2);
}
int main()
{
string str;
cin >> str;
cout << process(str, 0, str.size() - 1) << endl;
return 0;
}
题目2
给定一个只由0和1组成的字符串S,假设下标从1开始,规定i位置的字符价值V[i]订计算方式如下:
- i == 1时,V[i]=1
- i > 1时,如果S[i] != S[i-1],V[i] = 1
- i > 1时,如果S[i] == S[i-1],V[i] = V[i-1] + 1
你可以随意删除S中的字符,返回整个S的最大价值
字符串长度<=5000
思路
从左到右的尝试模型,当前位置有两种可能性,保留当前位置和删除当前位置。将上一个位置的数字和之前积累的价值传给当前位置。
保留当前位置:当前位置的价值就是,如果上一个位置的数字与当前位置数字一样,则当前数字的价值为V[i-1] + 1,否则就为1
删除当前位置:当前位置的价值就是V[i-1]
两种可能性取最大
代码
int process(string str, int index, char lastNum, int lastValue)
{
if (index == str.size())
{
return 0;
}
int curValue = str[index] == lastNum ? lastValue + 1 : 1;
int p1 = process(str, index + 1, str[index], curValue);
int p2 = process(str, index + 1, lastNum, lastValue);
return max(p1, p2);
}
int main()
{
string str;
cin >> str;
cout << process(str, 0, '0', 0);
return 0;
}
题目3
给定一个字符串str,和一个正数k
返回长度为k的所有子序列中,字典序最大的子序列
思路
单调栈。从左到右依次进单调栈,当前字符的字典序小于等于栈顶字符的字典序才能进栈,否则一直弹出到栈空或能进栈为止。下面就会出现两种可能性:
- 遍历完后栈内字符数量大于等于k个,那么从栈底开始往上数k个字符,这k个就是最后的答案。
- 遍历到i位置,单调栈的size() + str.size() - i + 1 的结果等于k了,那证明如果再遍历下去可能最后遍历完的时候,单调栈内的字符数量会小于k,此时不再弹出或遍历了,直接将栈内所有字符与包括i位置往后的所有字符(加起来刚好等于k个)返回即可。
代码
int main()
{
string str;
int k;
cin >> str;
cin >> k;
vector<char> stack(str.size());
int size = 0;
for (int i = 0; i < str.size(); i++)
{
while (size > 0 && stack[size - 1] < str[i] && size + str.size() - i> k)
{
size--;
}
if (size + str.size() - i == k)
{
string ans = "";
for (int j = 0; j < size; j++)
{
ans += stack[j];
}
ans += str.substr(i, str.size() - i);
cout << ans << endl;
return 0;
}
stack[size++] = str[i];
}
string ans = "";
for (int i = 0; i < k; i++)
{
ans += stack[i];
}
cout << ans << endl;
return 0;
}
题目4
给定一个数组arr,当拿走某个数a的时候,其他所有的数都+a
请返回最终所有数都拿走的最大分数
比如: [2, 3, 1]
当拿走3时,获得3分,数组变成[5, 4]
当拿走5时,获得5分,数组变成[9]
当拿走9时,获得9分,数组变成[ ]
这是最大的拿取方式,返回总分17
思路
从大到小拿即可
代码
int main()
{
int n;
cin >> n;
vector<int> arr(n);
for (int i = 0; i < n; i++)
{
cin >> arr[i];
}
sort(arr.begin(), arr.end());
int ans = 0;
for (int i = arr.size() - 1; i >= 0; i--)
{
ans += (ans * 2) + arr[i]; //总结出的公式
}
cout << ans << endl;
return 0;
}
题目5
把一个01字符串切成多个部分,要求每一部分的0和1比例一样,同时要求尽可能多的划分
比如:01010101
01、01、01、01 这是一种切法,0和1比例为1 : 1
0101、0101 也是一种切法,0和1比例为1 : 1
两种切法都符合要求,但是那么尽可能多的划分为第一种切法,部分数为4
比如:00001111
只有一种切法就是00001111整体作为一块,那么尽可能多的划分,部分数为1
给定一个01字符串str,假设长度为N,要求返回一个长度为N的数组ans
其中ans[i] = str[0...i]这个前缀串,要求每一部分的0和1比例一样,同时要求尽可能多的划分下,部分数是多少
输入:str = "010100001"
输出:ans = [1, 1, 1, 2, 1, 2, 1, 1, 3]
思路
假设当前前缀0和1的比例为a : b,那么它分的的最多的份数的每一份的0和1的比例都为a : b。假设当前前缀是下标为0 ~ j,之前的某个前缀的0和1的比例也为a : b,它的下标是0 ~ i,那么i + 1 ~ j的比例也一定是a : b(数学方法可以证明),那么如果前面有n个前缀拥有a : b的比例,当前前缀最多能分到的份数就是n + 1份,所以就看当前前缀的a : b,在之前的前缀中出现过多少次。可以利用哈希表,每到一个前缀,就储存这个前缀的0 1比。
代码
int gcd(int a, int b)
{
if (a < b)
{
int tmp = a;
a = b;
b = tmp;
}
if (b == 0)
{
return a;
}
return gcd(b, a % b);
}
int main()
{
string str;
cin >> str;
int zeros = 0;
int ones = 0;
unordered_map<int, unordered_map<int, int>> pre;
vector<int> ans(str.size());
for (int i = 0; i < str.size(); i++)
{
if (str[i] == '0')
{
zeros++;
}
else
{
ones++;
}
if (zeros == 0 || ones == 0)
{
ans[i] == i + 1; //当前前缀全是1或全是0,那么每个字符分成一份就是答案
}
else
{
int g = gcd(zeros, ones); //得到a与b的最大公因数
int a = zeros / g; //分子
int b = ones / g; //分母
if (pre.count(a) == 0)
{
pre[a][0];
}
if (pre[a].count(b) == 0)
{
pre[a][b] = 1;
}
else
{
pre[a][b]++;
}
ans[i] = pre[a][b];
}
}
for (int i = 0; i < ans.size(); i++)
{
cout << ans[i] << endl;
}
return 0;
}
题目6
[0, 4, 7]:0表示这里石头没有颜色,如果变红代价是4,如果变蓝代价是7
[1, X, X]:1表示这里石头已经是红,而且不能改颜色,所以后两个数X无意义
[2, X, X]:2表示这里石头已经是蓝,而且不能改颜色,所以后两个数X无意义
颜色只可能是0、1、2,代价一定 >= 0
给你一批这样的小数组,要求最后必须所有石头都有颜色,且红色和蓝色一样多,返回最小代价
如果怎么都无法做到所有石头都有颜色、且红色和蓝色一样多,返回-1
思路
首先如果小数组的数量是奇数个,则返回-1。然后遍历所有数组,找到红色石头、蓝色石头和没有颜色石头的个数,就可以确定没有颜色的石头多少个需要分配给红色,多少个需要分配给蓝色。假设a个石头需要变成红色,b个需要变成蓝色。先把所有无色石头都变成红色(或者蓝色),然后统计代价,这时候看其中哪b个石头变成蓝色减少的代价最多,即数组下标1减去下标2的值最大的b个,把它们变成蓝色再统计新的代价即可。
代码
int main()
{
int n;
cin >> n;
vector<vector<int>> arr(n, vector<int>(3));
int red = 0;
int blue = 0;
int sum = n;
int ans = 0;
vector<int> redToBlue(n);
for (int i = 0; i < n; i++)
{
for (int j = 0; j < 3; j++)
{
cin >> arr[i][j];
if (j == 0)
{
if (arr[i][j] == 1)
{
red++;
}
else if (arr[i][j] == 2)
{
blue++;
}
}
if (j == 1)
{
ans += arr[i][1];
}
if (j == 2)
{
redToBlue[i] = arr[i][1] - arr[i][2];
}
}
}
if ((sum & 1) != 0 || blue > (sum / 2) || red > (sum / 2))
{
cout << -1 << endl;
return 0;
}
sort(redToBlue.begin(), redToBlue.end());
int needs = sum / 2 - blue;
for (int i = redToBlue.size() - 1; i >= redToBlue.size() - needs; i--)
{
ans -= redToBlue[i];
}
cout << ans << endl;
return 0;
}
题目7
给定一个数组arr,和一个数k,找到数组arr中数字种类数量小于等于k的最长子数组,返回它的长度。
思路
窗口。窗口左边界每次固定位置,右边界开始往外扩,扩到最远位置后统计本次答案。L吐出当前字符,往右移动一位,继续有边界往外扩,然后统计。直到左边界越界。
代码
int main()
{
int n, k;
cin >> n, k;
vector<int> arr(n);
for (int i = 0; i < n; i++)
{
cin >> arr[i];
}
unordered_map<int, int> Map;
int L = 0;
int R = 0;
int ans = 0;
for (; L < n; L++)
{
if (Map.count(arr[L]) != 0)
{
Map[arr[L]]--;
if (Map[arr[L]] == 0)
{
Map.erase(arr[L]);
}
}
while (R < arr.size() && Map.size() <= k)
{
if (Map.size() < k || (Map.count(arr[R]) != 0 && Map.size() == k))
{
Map[arr[R]]++;
R++;
}
else
{
break;
}
}
ans = max(ans, R - L);
}
cout << ans << endl;
return 0;
}
题目8(lc289)
根据 百度百科 ,生命游戏 ,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
- 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
- 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
- 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
- 如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。
示例 1:
输入:board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
输出:[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
示例 2:
输入:board = [[1,1],[1,0]]
输出:[[1,1],[1,1]]
思路
用二进制位的从低到高数第二位储存下一次是死还是活。准备一个函数判断当前位置下回合是死还是活,然后二进制位最低为储存的是当回合的死活情况,用二进制位从低到高数第二位储存下一次是死活情况,遍历每一个位置,把每个位置都设定好,然后再次遍历一遍,把每个数的二进制位都向右移动一位即可(">>")。
代码
int f(vector<vector<int>>& board, int i, int j) board[i][j]位置是否是活细胞
{
if ((i >= 0 && i < board.size()) && (j >= 0 && j < board[0].size()))
{
if ((board[i][j] & 1) == 1)
{
return 1;
}
}
return 0;
}
int neighbors(vector<vector<int>>& board, int i, int j) //board[i][j]周围有多少个活细胞
{
int ans = 0;
ans += f(board, i - 1, j - 1);
ans += f(board, i - 1, j);
ans += f(board, i - 1, j + 1);
ans += f(board, i, j - 1);
ans += f(board, i, j + 1);
ans += f(board, i + 1, j - 1);
ans += f(board, i + 1, j);
ans += f(board, i + 1, j + 1);
return ans;
}
void gameOfLife(vector<vector<int>>& board) //主函数
{
for (int i = 0; i < board.size(); i++)
{
for (int j = 0; j < board[0].size(); j++)
{
int n = neighbors(board, i, j);
if (n == 3 || ((board[i][j] & 1) == 1 && n == 2))
{
board[i][j] |= 2;
}
}
}
for (int i = 0; i < board.size(); i++)
{
for (int j = 0; j < board[0].size(); j++)
{
board[i][j] = board[i][j] >> 1;
}
}
}
题目9(lc279)
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
思路
- 任何一个自然数,拆成最少个完全平方数的和,这些完全平方数不会超过四项。
- 任何数如果消除4这个因子,再 mod 8 等于7,那么它最少一定是4个完全平方数相加
再看这个数开根号向下取整的平方等不等于原来的数,从而判断它本身是不是一个完全平方数。
然后再依次枚举是不是两个完全平方数的和,具体做法就是这个数依次减去1的平方、2的平方、3的平方......。看剩下的部分开根号向下取整的平方等不等于原来的数。
如果不是4个、不是1个、不是2个,那最后就返回3个。
代码
int numSquares(int n) {
while (n % 4 == 0)
{
n /= 4;
}
if (n % 8 == 7)
{
return 4;
}
int a = (int)sqrt(n);
if (a * a == n)
{
return 1;
}
for (int i = 1; i * i <= n; i++)
{
int j = sqrt(n - i * i);
if (i * i + j * j == n)
{
return 2;
}
}
return 3;
}
涉及知识
四平方和定理
题目10(lc277)
搜索名人
给定一个数组,长度为n,代表有n个人,编号分别是0 ~ n-1。
给定一个函数 knows(i, j) ,可以查询i认不认识j(单向查询,i认识j不代表j也认识i),认识返回true,不认识返回false
名人:数组中除了他自己其他人都认识他,而他不认识所有人。
假定每个人自己不认识自己
数组中可能有名人也可能没有,如果有就返回这个名人的编号,如果没有就返回-1。要求调用最少次数的knows函数
思路
首先看看数组里有没有可能是明星的人。当调用 knows(i, j) 返回true后,根据名人的定义,i一定不是名人,j才有可能是名人;如果返回false,那么j一定不是名人,i可能是名人,因为所有人都必须认识名人,而名人不认识所有人。准备一个变量candidate,储存可能是名人的那个人的编号,初始值是0。从左到右遍历数组,每次查询knows(candidate, i),如果返回true,那么将candidate变为i,返回false,排除i不是名人,candidate不变。遍历完数组后就会得到一个可能是名人的编号。然后再次遍历数组,调用knows(candidate, i),看看这个疑似名人认不认识其他人,一旦他认识其他任何一个人,返回-1。如果上一步没有返回-1,那么最后再遍历一遍数组,调用knows(i, candidate),看看是不是所有人都认识这个疑似名人,一旦有人不认识,那么就返回-1。以上筛选流程都通过的就一定是名人。
代码
int findCelebrity(int n)
{
int candidate = 0;
for (int i = 0; i < n; i++)
{
if (knows(candidate, i))
{
candidate = i;
}
}
for (int i = 0; i < n; i++)
{
if (knows(candidate, i))
{
return -1;
}
}
for (int i = 0; i < n; i++)
{
if (!knows(i, candidate))
{
return -1;
}
}
return candidate;
}
题目11(lc198)
打家劫舍Ⅰ
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
思路
动态规划。dp[i]含义:0......i范围内能偷到的最多的钱数是多少。
分情况讨论:
- 我只偷当前位置,那么 dp[i] = arr[i]。
- 我一定不偷当前位置,那么dp[i] = dp[i-1]。
- 我偷当前位置,那么dp[i] = arr[i] + dp[i-2]。
两种情况的最大值就是当前的dp[i]的值。
dp[0] = arr[0],dp[1] = max(arr[0], arr[1])。剩下的就可以用上面的情况分析进行更新了。
代码
int main()
{
int n;
cin >> n;
vector<int> arr(n);
for (int i = 0; i < n; i++)
{
cin >> arr[i];
}
if (n < 2)
{
cout << arr[0] << endl;
return 0;
}
vector<int> dp(n);
dp[0] = arr[0];
dp[1] = max(arr[0], arr[1]);
for (int i = 2; i < n; i++)
{
dp[i] = arr[i];
dp[i] = max(dp[i], dp[i - 1]);
dp[i] = max(dp[i], dp[i - 2] + arr[i]);
}
cout << dp[n - 1] << endl;
return 0;
}
题目12(lc213)
打家劫舍Ⅱ
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
思路
利用打家劫舍Ⅰ的思路,n为arr.size(),在arr的0 ~ n-2范围内求出一个答案,再在1 ~ n-1范围内求出一个答案,返回两个答案的最大值
代码
int rob1(vector<int>& arr, int i, int j);
int rob(vector<int>& nums) //主函数
{
if (nums.size() < 1)
{
return 0;
}
if (nums.size() < 2)
{
return nums[0];
}
return max(rob1(nums, 0, nums.size() - 2), rob1(nums, 1, nums.size() - 1));
}
int rob1(vector<int>& arr, int i, int j)
{
int n = j - i + 1;
if (n < 2)
{
return arr[i];
}
int pre1 = arr[i];
int pre2 = max(arr[i], arr[i + 1]);
for (int k = i + 2; k <= j; k++)
{
int cur = max(pre2, pre1 + arr[k]);
pre1 = pre2;
pre2 = cur;
}
return pre2;
}
题目13
给定一个数组arr,数组上每个下标代表一个题目,下标对应的数组中的值代表题目的难度,不同下标代表不同题目即使难度相同。
给定一个数m,规定任何两道相邻的题,前一道题的难度不能超过后一道题的难度+m,所有的题目都要参与。
返回数组arr中的题目在符合规定的条件下可以出多少种卷子
思路
动态规划,dp[i]含义:0 ~ i之间所有的题目都用上,可以出几种前一道题的难度不能超过后一道题的难度+m的卷子。先给数组从小到大排序,这样做的原因是要排除一种情况:前面不符合要求的一种排列在后面可以变得符合要求。比如:m = 3,[7, 3, 3]这种情况是不符合要求的,但是如果后面遇到一个难度为4的题目, [7, 4, 3, 3]就变得符合要求了,从小到大排序的原因就是为了杜绝这种情况,因为后面的题难度只会越来越大,至少会比7大或者相等,这样怎么插入之前不符合要求的排列都不会使其变成符合要求的。
接下来讨论dp[i]如何更新,有以下两种情况:
- 由于排过序,所以后面的题目是难度越来越大的(或者难度相等),所以在前面所有排列后面直接加上当前i位置的题目就是另外的符合要求的排列。即可能性1:p1 = dp[i-1]
- 假设当前i位置的题目难度为a,那么题目数组中在i前面有多少个题难度是大于等于a-m的题目,在他们前面插上i位置的题目也都是符合要求的,假设题目数组中在i前面有num个题难度是大于等于a-m的题目,即可能性2:p2 = num * dp[i - 1]
dp[i]的值就是上面两种情况相加,即dp[i] = dp[i-1] + num * dp[i - 1] = (num + 1) * dp[i - 1]
浙公网安备 33010602011771号