绪论
上周因为有事没有参加周赛,这周没有错过。这次周赛拿到了人生第一个AK,参加大大小小的比赛这么多次,从来没有AK过,泪目了。
感觉这次比赛的思维难度对我来讲稍高一些,前三道题就花了一个小时,而以往只需要半个小时。
看了一下排名前面的大牛们,还是十分钟就AK了,深觉自己还马达马达大内。
题目分析
比赛链接:https://leetcode-cn.com/contest/weekly-contest-286/
题目难度上第二题和第三题都有一些思维量,不像以前直接模拟。第四题我直接记忆化搜索在最后一分钟过了,当时学习动态规划的时候接触到记忆化搜索对其嗤之以鼻,觉得就是弱者想不出来状态转移方程,用这种近似暴力的方式来处理。然后现在发现,弱者竟是我自己,记忆化搜索真香。
A:找出两数组的不同
签到题,数据量很小,也没有仔细想直接两个哈希集合,去重并判断每个元素是否在另一个集合出现过,没有出现过就添加到结果数组中。
class Solution {
public:vector<vector<int>> findDifference(vector<int>& nums1, vector<int>& nums2) {unordered_set<int> s1, s2;for (auto x : nums1) s1.insert(x);for (auto x : nums2) s2.insert(x);vector<vector<int>> ret(2);for (auto x : s1) {if (s2.count(x) == 0) ret[0].push_back(x);}for (auto x : s2) {if (s1.count(x) == 0) ret[1].push_back(x);}return ret;}
};
B:美化数组的最少删除数
题目的意思就是偶数位置的元素要和下一个位置的元素不相等。因为只向后看, 所以当时想到了一个构造答案的方法:首先统计每个数字连续出现的次数x。对于偶数位置,ans+=x-1,对于奇数位置,ans+=x-2。最后如果要填充奇数位置,则++ans,因为最后一个位置必须要保证数组的长度为偶数。
class Solution {
public:int minDeletion(vector<int>& nums) {vector<pair<int, int>> cnt;int n = nums.size();for (int i = 0; i < n; ) {int j = i + 1;while (j < n && nums[j] == nums[i]) ++j;cnt.emplace_back(nums[i], j - i);i = j;}n = cnt.size();int ans = 0, x;bool is_even = true;for (int i = 0; i < n; ++i) {x = cnt[i].second;if (is_even) {ans += x - 1;is_even = false;} else {if (x == 1) {is_even = true;} else {ans += x - 2;}}}if (!is_even) ++ans;return ans;}
};
这样做的正确性在于,对于偶数位置,他如果连续出现了多次,最多只能保留1个。对于奇数位置,如果连续出现了多次,最多只能保留2个。这里我们每次都选择的是尽可能保留以满足题目中的最短长度。最后我们处理了一下让整个数组的长度为偶数:如果下一次要填充的是奇数位置的数字,那么说明前面的位置是偶数位置,需要将其删除。
接下来我们简单讨论一下为什么尽可能保留数字是最优的。假如某个位置我们可以保留某个数字但是我们将其删光了,后面的数字会移动到前面,同样需要删除,并不能让解更好。详细的证明需要分类讨论之类的,这里我们就不求甚解了。
C:找到指定长度的回文数
是一个对我来讲有点思维量的模拟,我们需要能够构造任意长度,第任意大小的回文串。为了能够构造第x大的回文串,我们需要使用类似进制转换的思想。
对于相同长度的回文串,其值和相对大小是由前面一半的数字支配的,后面一半的数字都不用进行考虑。
第一个数字只能是1-9,后面的数字每一位都可以是0-9。对于一个回文串长度为intLength,他的所有可能结果是maxn=9∗10intLength−12maxn = 9*10^{\frac{intLength -1}{2}}maxn=9∗102intLength−1。如果x大于maxn,则直接返回-1。
接下来我们来从前往后确定每一位数字的大小。第一位数字确定后,后面的数字有maxn/=9种可能。即第1——maxn个回文串的第一位是1,第maxn+1——2maxn个回文串的第一位是2。为了确定第一位的数字,我们可能想要让x/maxn来确定。但是整除需要我们特别处理一下。
因为算数运算默认是从0开始的,0——maxn-1 /x都是0。为了寻求这种统一,我们不妨给x减一,从而可以直接套用算术运算。
对于后面的位数也是同样的道理。总结一下就是为了能够让x对固定步长(上面的maxn)进行分组,我们让x-1,从而将原本1——maxn变成了0——manx-1,变成了在数值意义上的同一组。
后面取余仍然会从0开始,所以我们只用减一一次。
class Solution {using ll = long long;ll n_, maxn_, len_;ll work(ll x) {--x;vector<int> arr;ll n = n_;arr.push_back(x / n + 1);x %= n;ll len = len_;len -= 2;while (len > 0) {n /= 10;arr.push_back(x / n);x %= n;len -= 2;}ll ret = 0;for (auto x : arr) ret = ret * 10 + x;if (len_ & 1) arr.pop_back();int nn = arr.size();for (int i = nn - 1; i >= 0; --i) ret = ret * 10 + arr[i];return ret;}public:vector<long long> kthPalindrome(vector<int>& queries, int intLength) {len_ = intLength;int n = (intLength - 1) >> 1;n_ = 1;for (int i = 0; i < n; ++i) {n_ *= 10;}maxn_ = n_ * 9;vector<ll> ret;for (auto x : queries) {if (x > maxn_) ret.push_back(-1);else {ret.push_back(work(x));}}return ret;}};
仔细研究了一下大牛的解法,发现我这里处理复杂的原因是没有想到每位填充的也是十进制数字,那么x-1+maxn就是前一半数字。x-1是第一位从0开始的第x个数字,加上maxn就是第一位从1开始的。
D:从栈中取出 K 个硬币的最大面值和
我们很容易就可以计算出从一个栈中取m个硬币的面值和,那么问题就是我们可以从n个栈中取硬币,每个栈可以取0个或多个,最终取k个的最大和。想了一下也没有什么状态转移的,就直接记忆化搜索了。
class Solution {int n_;vector<vector<int>> sum;vector<int> cnt;vector<vector<int>> memo;int dfs(int x, int k) {if (x == n_) {return sum[x][k];} else {if (memo[x][k] != -1) return memo[x][k];int kk = std::min(k, (int)sum[x].size() - 1);for (int i = std::max(0, k - cnt[x]); i <= kk; ++i) {memo[x][k] = max(memo[x][k], sum[x][i] + dfs(x + 1, k - i));}}return memo[x][k] == -1 ? INT_MIN : memo[x][k];}
public:int maxValueOfCoins(vector<vector<int>>& piles, int k) {int n = piles.size();memo.resize(n, vector<int>(k + 1, -1));n_ = n - 1;sum.resize(n);cnt.resize(n);for (int i = 0; i < n; ++i) {auto &arr = piles[i];auto &s = sum[i];s.push_back(0);for (auto x : arr) {s.push_back(s.back() + x);}}int t = 0;for (int i = n - 1; i >= 0; --i) {cnt[i] = t;t += piles[i].size();}return dfs(0, k);}
};
当时最后几分钟写完后一直运行错误,我心态有点崩,觉得果然又要到此为止了吗。但是还是耐下性子去看代码到底哪里有问题。当时报的是堆上的错误,我就觉得是不是哪里数组越界了。认真一看,sum、cnt不可能越界,那是不是备忘录memo越界了呢?仔细一想,memo的第二个维度是可以取到k的,而我第二个维度的大小只有k,于是将第二个维度的大小改成k+1就过了。
以前写记忆化搜索都是自己特化一个对pair的哈希然后用unordered_map做,这次因为时间不够用的二维数组,而之前都没有写过用二维数组进行备忘录,所以就没注意过这个问题。
虽然时间紧迫,但是我对自己这个记忆化搜索还是挺满意的,有备忘录,有必要的剪枝,代码也很紧凑。
首先初始化了一下memo和sum数组,sum[x][k]表示的是第x个栈取k个硬币的面值和,memo[x][k]表示的是对于从x到n-1的栈,总共取k个硬币的最大面值和,最终的答案就是memo[0][k],初始化为-1表示没有进行更新,如果memo[x][k]不可能存在,则赋值为无穷小。因为我们求的是最大值,所以不会用到这个状态。
cnt数组是为了剪枝维护的状态,cnt[x]表示从x+1到n-1总共有多少个硬币,std::max(0,k-cnt[x])
就表示第x个栈至少要取多少枚硬币才能够保证从x到n-1能够取到k个硬币,std::min(k, (int)sum[x].size()-1)
表示第x个栈至多能够取到多少个硬币。
总共最多有O(x)*O(k)=2e6个状态,每个状态至多求解一次,每个状态的求解至多是O(k),因此最坏的时间复杂度是2e9。本来心里有些打鼓,但是提交后发现通过了,非常开心。
仔细阅读了一下大牛的解法,发现是一个01背包问题。感觉自己对背包问题的理解还是不够深刻,应该专门再整理一下背包问题的思路。