第一题 1656. 设计有序流
可以开一个字符串数组存放字符串,用一个额外的变量ptr存放ptr的位置,插入新的字符串的时候,检查从ptr开始是否有连续的按照id递增的序列即可。
代码如下:
class OrderedStream {
public:
int n;
int ptr;
vector<string> stream;
OrderedStream(int _n) {
n = _n;
ptr = 1;
stream = vector<string>(n + 1, ""); // 字符串数组中所有字符串最初都为空
}
vector<string> insert(int id, string value) {
stream[id] = value;
if(stream[ptr] == "") { // 如果插入一个新字符串,但id=ptr的字符串为空,则返回一个空的字符串数组
return {};
}
vector<string> res;
int last = ptr; // last记录更新后的ptr的位置
for(int i = ptr; i <= n; ++i) {
if(stream[i] != "") {
res.push_back(stream[i]);
} else { // 找到一个空的位置,则更新last
last = i;
break;
}
}
if(stream[last] != "") {
ptr = last + 1;
} else {
ptr = last;
}
return res;
}
};
/**
* Your OrderedStream object will be instantiated and called as such:
* OrderedStream* obj = new OrderedStream(n);
* vector<string> param_1 = obj->insert(id,value);
*/
第二题 5603. 确定两个字符串是否接近
根据题意,两个字符串接近,则满足这些条件:(1)两个字符串中不同的字母的个数相同;(2)如果给所有的字母统计出现次数,则如果某字符串中有出现次数为x的字母y个,则另一个字符串也必然有y个出现次数为x的字母;(3)第一个字符串中的所有字母都在第二个单词内出现过,反之亦然。
第(1)和第(3)点我们可以使用两个unorderd_set进行判断。
第(2)点可以用两个unordered_map<char, int> cnt1, cnt2统计两个字符串中各个字母出现的频率,然后用两个字符串(或数组)将哈希表中的所有频率拼接为一个字符串,如果原单词word1和word2是“接近”的字符串,则将它们的字母出现频率拼接成的字符串排序之后,这两个排序后的字符串必然是相等的,如果不相等,则它们不接近。这一点很好理解,比如word1里有若干个单词出现次数为:2 1 3 4, word2里有若干个单词出现次数为:1 2 4 3,那么它们的出现次数拼接的单词排序之后都是1234,所以这两个单词是接近的。
代码如下:
class Solution {
public:
unordered_set<char> hash1, hash2; // 计算两个单词中不同单词的个数
unordered_map<char, int> cnt1, cnt2; // 统计两个单词中不同字母出现的次数
bool closeStrings(string word1, string word2) {
if(word1.size() != word2.size()) { // 如果两个单词长度不同,那么它们不接近
return false;
}
for(auto &c : word1) {
hash1.insert(c);
++cnt1[c];
}
for(auto &c : word2) {
hash2.insert(c);
++cnt2[c];
}
for(auto &c : hash1) {
if(hash2.count(c) == 0) { // 如果第一个单词有第二个单词没有出现过的字母,则它们不接近
return false;
}
}
for(auto &c : hash2) {
if(hash1.count(c) == 0) { // 如果第二个单词有第一个单词没有出现过的字母,则它们不接近
return false;
}
}
string fre1, fre2; // fre1和fre2分别是对word1和word2的单词出现的频率拼接得到的字符串
for(auto &cnt : cnt1) {
fre1 += to_string(cnt.second);
}
for(auto &cnt : cnt2) {
fre2 += to_string(cnt.second);
}
sort(fre1.begin(), fre1.end());
sort(fre2.begin(), fre2.end());
if(fre1 != fre2) {
return false;
}
return true;
}
};
第三题 5602. 将 x 减到 0 的最小操作数
题意要求在数组的两端不断地删除元素,使得删掉的元素的和为x,求最小的删除次数。可以反向思考:求数组中最长的区间和为sum - x的区间,其中sum为数组中所有元素的和。
求数组中的区间和可以使用前缀和,然而枚举区间的起点和终点时间复杂度仍会达到O(n2),对于105的区间长度是无法接受的。
因此我们可以考虑用一个哈希表unordered_map<int, int> mp来记录某个前缀和的下标,mp[i] = j表示前缀和为i的下标为j,即nums[0] + nums[1] + … nums[j] = i, 我们要找出区间和target = sum - x的区间,而我们在循环的时候可以知道当前位置i的前缀和preSum[i], 因此我们只要知道前缀和为preSum[i] - target的位置,我们就得到了满足题意的一段区间和(也就是说,删掉这段区间的左边部分和右边部分,可以满足题意),然后我们可以用数组元素的个数 - 这段区间的长度来更新答案。 使用哈希表记录前缀和的下标,我们可以在O(n)的时间复杂度内求出满足条件的区间和和区间长度。
代码如下:
class Solution {
public:
vector<int> preSum;
unordered_map<int, int> mp; // 记录某前缀和的下标
int minOperations(vector<int>& nums, int x) {
if(nums[0] > x && nums.back() > x) { // 如果数组的第一个和最后一个元素都大于x,返回-1
return -1;
}
int n = nums.size();
preSum = vector<int>(n + 1, 0);
int res = n + 1;
mp[0] = 0; // 0的前缀和为0
for(int i = 1; i <= n; ++i) {
preSum[i] = preSum[i - 1] + nums[i - 1];
mp[preSum[i]] = i; // i的前缀和为preSum[i]
}
int sum = preSum[n]; // sum是数组的所有元素的和
if(x > sum) {
return -1;
}
if(sum == x) { // 删掉所有元素
return n;
}
int target = sum - x; // 要求的区间和target为sum - x
for(int i = 1; i <= n; ++i) {
if(mp[preSum[i] - target] == 0) { // 之前没记录过前缀和为preSum[i] - left的位置
if(preSum[i] == target) { // 如果preSum[i]正好就是target,那么说明我们要删掉i后面的所有元素,可以满足删掉的元素的和为x
res = min(res, n - i);
}
} else { // 之前记录过前缀和preSum[i] - target的位置,也就是说,我们找到了一个满足条件的区间[left ~ i]
int left = mp[preSum[i] - target];
res = min(res, n - (i - left)); // 更新最小操作数:区间和为sum - x的区间长度为i - left, 则这段区间左边部分与右边部分的长度为n - (i - left)
}
}
return res;
}
};
第四题 1659. 最大化网格幸福感
状态压缩DP,这里参考了坑神的视频题解
用dp[r][i][e][s]表示前r行网格(1<=r<=n)、居住了i个内向的人(0<=i<=introvertsCount)、e个外向的人(0<=e<=extrovertsCount)、并且第r行的居住人的分布状态为s的最大网格幸福感,s的三进制表示是第r行的分布。如果第r行的第j个网格(0<=j<m)没有住人,则s的第j位为0;如果住了一个内向的人,则s的第j位为1;如果住了一个外向的人,则s的第j位为2。
则整个网格的最大幸福感是,处理了前n行,最多居住introvertsCount个内向的人、最多居住extrovertsCount个外向的人,分布状态为s(0<=s<3^m)的网格幸福感的最大值。
有了状态表示,我们要考虑状态转移。
首先,由于我们是按行枚举,所以我们对每一行,都要考虑上一行对这一行的影响(影响指:内向的人数的变化、外向的人数的变化、幸福感的变化)。另外,由于这一行的人员分布状态也可能影响上一行幸福感(比如这一行住了人,会影响到上一行的内向的人和外向的人的幸福感),因此我们在枚举某行的状态和上一行的状态时,要考虑到互相的影响,具体的分析写在下面的注释里。
代码如下:
int dp[7][7][7][250]; // dp[r][i][e][s]:前r行使用了i个内向的人、e个外向的人,分布状态为s的最大网格幸福感。 因为一行最多有5个格子,3^5=243,所以分布状态s的范围为0~242
int base[6]; // base表示每一列是3的多少次幂。第0列base[0] = 3^0 = 1, 第1列base[1] = 3^1 = 3, 第2列base[2] = 3^2 = 9, 第3列base[3] = 3^3 = 27,第4列base[4] = 81, 第5列base[5] = 243。 base数组的目的是为了方便为门计算状态s的某一位是多少(0/1/2)
int movIn[250][250], movEx[250][250], mov[250][250]; // movIn[i][j]表示从某行的人员分布状态i转移到下一行的人员分布状态j内向的人数的变化,movEx[i][j]表示外向的人数的变化,mov[i][j]表示幸福感的变化
class Solution {
public:
int get(int s, int i) { // 计算状态s的第i位是多少。 返回值=0:没人 =1:内向的人 =2:外向的人
s = s % base[i + 1];
return s / base[i];
}
int getMaxGridHappiness(int m, int n, int introvertsCount, int extrovertsCount) {
memset(dp, -1, sizeof dp);
dp[0][0][0][0] = 0;
base[0] = 1;
for(int i = 1; i <= 5; ++i) {
base[i] = base[i - 1] * 3;
}
int lim = base[m]; // lim是某一行状态的最大值+1,也就是说,某一行的状态s的取值范围为0 ~ lim - 1
// 预处理出所有状态到其他状态造成的movIn, movEx, mov的变化
for(int s = 0; s < lim; ++s) { // 枚举上一行的状态s
for(int cur = 0; cur < lim; ++cur) { // 枚举当前行的状态cur
movIn[s][cur] = movEx[s][cur] = mov[s][cur] = 0; // 先假设上一行的状态转移到当前行的状态,内向的人数、外向的人数、幸福感都没有变化
for(int i = 0; i < m; ++i) { // 枚举当前行的每一列
if(get(cur, i) == 0) { // 如果当前行的第i列没有住人
continue; // 那么它对上一行没有任何影响
} else {
if(get(s, i) == 1) { // 如果当前位置住了人并且上一行的这个位置住了个内向的人,那么幸福感-30
mov[s][cur] -= 30;
} else if(get(s, i) == 2) { // 如果当前位置住了人并且上一行的这个位置住了个外向的人,那么幸福感+20
mov[s][cur] += 20;
}
}
if(get(cur, i) == 1) { // 如果当前位置住了个内向的人
movIn[s][cur] += 1; // 更新内向的人数的变化
mov[s][cur] += 120; // 内向的人的初始幸福感是120
if(get(s, i) > 0) { // 如果上一行这一列位置住了人,那么幸福感-30
mov[s][cur] -= 30;
}
if(i > 0 && get(cur, i - 1) > 0) { // 如果左边位置住了人,幸福感-30
mov[s][cur] -= 30;
}
if(i + 1 < m && get(cur, i + 1) > 0) { // 如果右边位置住了人,幸福感-30
mov[s][cur] -= 30;
}
}
if(get(cur, i) == 2) { // 如果当前位置住了一个外向的人
movEx[s][cur] += 1; // 更新外向的人数
mov[s][cur] += 40; // 外向的人初始幸福感是40
if(get(s, i) > 0) { // 如果上一行这一列住了个人
mov[s][cur] += 20; // 幸福感+20
}
if(i > 0 && get(cur, i - 1) > 0) { // 如果左边位置住了人,幸福感+20
mov[s][cur] += 20;
}
if(i + 1 < m && get(cur, i + 1) > 0) { // 如果右边位置住了人,幸福感+20
mov[s][cur] += 20;
}
}
}
}
}
// 动态规划
for(int r = 1; r <= n; ++r) { // 枚举1~n行
for(int i = 0; i <= introvertsCount; ++i) { // 枚举内向的人的个数
for(int e = 0; e <= extrovertsCount; ++e) { // 枚举外向的人的个数
for(int s = 0; s < lim; ++s) { // 枚举上一行(第r - 1行)的状态s
if(dp[r - 1][i][e][s] == -1) {
continue;
}
for(int cur = 0; cur < lim; ++cur) { // 枚举当前行的状态cur
int deltaIn = movIn[s][cur], deltaEx = movEx[s][cur], delta = mov[s][cur]; // deltaIn, deltaEx, delta表示上一行的状态s到这一行的状态cur引起的内向的人数、外向的人数、幸福感的变化
if(i + deltaIn <= introvertsCount && e + deltaEx <= extrovertsCount) { // 如果内向的人数和外向的人数的个数分别不超过introvertsCount和extrovertsCount,则更新最大幸福感
dp[r][i + deltaIn][e + deltaEx][cur] = max(dp[r][i + deltaIn][e + deltaEx][cur], dp[r - 1][i][e][s] + delta);
}
}
}
}
}
}
int ans = 0; // 我们要求出最后一行(第n行)的最大幸福感,就是最终的答案
for(int i = 0; i <= introvertsCount; ++i) { // 枚举内向的人的个数、外向的人的个数、以及第n行的人员分布状态
for(int e = 0; e <= extrovertsCount; ++e) {
for(int s = 0; s < lim; ++s) {
ans = max(ans, dp[n][i][e][s]);
}
}
}
return ans;
}
};