[cf rating 1000-1600] 刷题笔记1
【King's Task】:
0.准确理解题意,注意permutation这个词,表示排列,这题要求的是能否通过一定次数的操作使一个1-2*n的排列从小到大排好序,最终的状态是确定的,即,1,2,3,...,2*n
1.提供了两种操作,注意到,这两种操作都具有自逆性(self-inverse),也就是说,连续两次做同一个操作不会发生变化。所以两种操作应该交替进行,根据第一次选择的操作分出两种情况,分别求解。
2.操作终止条件:由1得交替进行两种操作,那么何时停止呢?如果操作得到了目标状态,那么自然就停止了。而如果是无解的情况呢?这时候要考虑到两种操作的周期性。
两种操作分别是:1.交换奇数位与偶数位 swap(a[i],a[i+1]) 2.与第n位后的交换 swap(a[i],a[i+n])
假设第一次选择操作1,对第i(设i为奇数,且i<=n)位的数k,则k的位置变化为:
i -> i+1 -> i+n+1 -> ?
此时由于n的奇偶性不确定,所以第三次操作不确定,因此我们继续对n的奇偶性讨论
假设n为奇数:
i -> i+1 -> i+n+1 -> i+n+2 -> i+2 -> i+3 -> i+n+3 -> i+n+4 -> i+4 -> ...
不断递推下去可以得到 i -> ...... -> i+n 此时构成循环
我们可以把第一个样例推一遍,易得周期为2*n
假设n为偶数:
i -> i+1 -> i+n+1 -> i+n -> i
此时只需要4次操作即构成一个循环,周期为4
3.小结:这题属于略难的模拟(implementation),首先要读明白题意,然后要通过样例推导出正确的模拟方法。
【Restoring the Permutation】:
1.这题的贪心其实是很明显的,重点在于找出满足条件的当前未访问的最小/大值
2.应当使用合适的数据结构降低时间复杂度,二分查找出合适的值。可以使用stl set实现。
【Epic Transformation】:
1.跟上一题一样,贪心的思路挺好想的:让当前map中value值最大的两个-1。
2.实现的话可以灵活组合stl,整体用个优先队列,存储用map,子结构用pair。学习了tutorial上比较高级的写法,贴在下面:
#include <bits/stdc++.h> using namespace std; void solve(){ int n; cin>>n; map<int,int> v; for(int i=1,x;i<=n;++i){ cin>>x; v[x]++; } priority_queue<pair<int,int>> pq; for(auto [x,y]: v){ pq.push({y,x}); } int ans = n; while(pq.size()>=2){ auto [cnt1, x1] = pq.top(); pq.pop(); auto [cnt2, x2] = pq.top(); pq.pop(); cnt1--,cnt2--; ans -= 2; if(cnt1) pq.push(make_pair(cnt1,x1)); if(cnt2) pq.push(make_pair(cnt2,x2)); } cout<<ans<<endl; } int main() { ios::sync_with_stdio(false); cin.tie(0); int tt; cin >> tt; while (tt--) { solve(); } return 0; }
【Double-ended Strings】:
0.题意分析:每次操作可以删除a/b的首/尾字母,要求操作多少次后a=b(可以都是空字符串)。a,b最后相等的部分必然是原字符串中连续的一部分,因为只能在首尾删除。因此,这就转化成最长公共子串的问题。ans = a.size()+b.size()-maxLen*2。
1.最长公共子串的dp方程:
设dp[i][j]表示a的以i结尾的子串和b的以j结尾的子串的最长公共子串长度。
dp[i][j] = 1. if a[i]==b[j]: dp[i-1][j-1]+1 2. if a[i]!=b[j]: 0
注意:答案并不是dp[a.size()][b.size()],也有可能是中间的某个状态,所以随时更新答案。(这与最长公共子序列不同)
2.扩展:最长公共子序列:不同之处在于不要求序列连续
设dp[i][j]表示a的子串[0..i]和b的子串[0..j]的最长公共子序列长度。(注意与最长公共子串dp的定义不同)
dp[i][j] = 1. if a[i]==b[j]: dp[i-1][j-1]+1 2. if a[i]!=b[j]: max(dp[i-1][j], dp[i][j-1])
最后的答案为dp[a.size()][b.size()],因为这次的dp数组具有包含性。
Tip: 边界条件都是0,所以dp数组的下标都从1开始
Tip: https://blog.csdn.net/afei__/article/details/83153399 可以看看图理解
当然这题的数据规模较小,tutorial里给的是暴力的解法:枚举maxlen从1-min(n,m),提取a、b等于maxlen的子串看有没有相等的。
【Partial Replacement】:
还是贪心:从第一个'*'(位置i)开始,在满足 j<s.size() && j-i<=k && s[j]=='*' 的条件下寻找最大的j。
代码实现还是需要好好想一想的,这一块得提高。
#include <bits/stdc++.h> using namespace std; void solve(){ int n,k; cin>>n>>k; string s; cin>>s; int i = s.find_first_of('*'); int res = 1; while(true){ int j = min(n-1,i+k); for(;i<j && s[j]=='.';j--); if(i==j) break; res++; i = j; } cout<<res<<endl; } int main() { ios::sync_with_stdio(false); cin.tie(0); int tt; cin >> tt; while (tt--) { solve(); } return 0; }
tutorial的这个代码写得还是很精妙的,值得好好学习。
再贴一个rank 1的代码,逻辑相比之下更易懂:
void solve() { int N, K; cin >> N >> K; string S; cin >> S; int p = -1; F0R(i, sz(S)) { if (S[i] == '*') { p = i; break; } } int ans = 1; while (true) { bool found = false; FORd(i, p+1, min(N, p+K+1)) { // 前闭后开区间,倒序遍历 if (S[i] == '*') { p = i; ans++; found = true; break; } } if (!found) break; } cout << ans << nl; }
【Balance the Bits】:
这是一道构造题,首先要判断什么情况下可以构造出来,什么情况下不行。然后还要找到一种构造的策略。
1.首先,s的首尾必须都是'1',这没什么疑问。其次我们注意到n是个偶数(题目条件),则生成的两个括号序列a,b中左括号的数目之和也应该是个偶数,等于n。分析给的s中'1'和'0'会对左括号的数目产生怎样的影响:'1'会使左括号数目+2/+0;'0'会使左括号数目+1,而最终和为偶数,则s中'0'的个数必须是偶数。最后,满足了以上条件是否就对了呢?我们不妨先尝试一下能否构造出来。
2.满足1中条件的字符串s有这样的特性:'1'和'0'的数目均为偶数。由于最后要使左右括号的数目相等,所以前一半的'1'要生成左括号,后一半的‘1’要生成右括号。而‘0’的处理则是关键,先生成左括号还是右括号并不重要,因为a,b两个序列'0'位置上的必然不同,重要的是要让左右括号间隔出现,这样才能最大程度匹配上。这样就构造出了结果,括号的数目是没什么问题的,重点是能否使左右括号完美的匹配上。关于这一点我还是不太确定,只能说现在这种构造方式应该是最优的了。
这种构造题,判断能否一般都是看数论的奇偶性分析,而构造则要一点灵性了。
【Flip the Bits】:
这道题昨天困了我好久,题目要求能不能通过有限次特定操作使a变为b。操作是把a的一个满足'0'和'1'数目相等的前缀翻转,也就是做一个按位取反操作。
我们注意到,这个前缀经过任意次操作后依旧满足操作的条件,当然连续的两次操作没有意义,而不满足要求的前缀就永远无法满足操作要求了。
我们可以把a能够满足条件的前缀的末端点都找出来,将起点和所有这样的末端点构成的互不重叠的区间设为a1,a2,a3...。为了便于说明思路,假设这样的区间有三个,分别是a1,a2,a3,而最后还剩下一段对任何一个满足条件的前缀操作都无法改变的区间,就不用管了。
设ai按位取反后的区间为~ai,a1+a2表示合并的一个区间
那么对这些区间操作可以产生怎样的效果呢?
1.取反的最小作用单元为一个区间,即一个区间要么是a1,要么是~a1,只有两种状态。
2.可以使a1,a2,a3这三个区间取各自两种状态下的任意一种。
证明:假设要使初始状态a1,a2,a3变为a1,~a2,a3,即只改变a2的状态
可以这样操作:a1,a2,a3 -> ~(a1+a2),a3 = ~a1,~a2,a3 -> a1,~a2,a3
想要改变任意一个独立区间的状态都可以这样类似的操作。
明白了操作的可能结果,我们能就可以尝试对a进行操作了:对划分出来的每个区间,若区间第一个元素不等于b相同位置的元素,则对此区间取反,直到所有区间处理完毕,最后考察操作后的a是否与b相同。这样操作已经是尽最大努力使a和b相同了,若这样都不行,那必然无法实现。(有点贪心的思想在)
#include <bits/stdc++.h> using namespace std; void solve(){ int n; cin>>n; string a,b; cin>>a>>b; int i=0; // 区间起始坐标 int cnt0 = 0, cnt1 = 0; for(int j=0;j<a.size();++j){ if(a[j]=='0') cnt0++; else cnt1++; if(cnt0==cnt1){ if(a[i]!=b[i]){ // 翻转此区间 for(int k=i;k<=j;++k){ if(a[k]=='0') a[k]='1'; else a[k]='0'; } } i = j+1; } } if(a==b) cout<<"YES"<<endl; else cout<<"NO"<<endl; } int main() { ios::sync_with_stdio(false); cin.tie(0); int tt; cin >> tt; while (tt--) { solve(); } return 0; }
【Minimum Grid Path】:
做多了各种奇奇怪怪的思维题,暴力算法反而一时想不到了。
这题只需要按照选择的线段条数k分类讨论(范围[2,n]),对于每种情况贪心求出最小代价,更新答案即可。
具体的贪心策略:首先注意到只有改变方向才能增加线段条数,所以应该分为两组。设有k条线段,代价分别是c[1],c[2],...,c[k]。设第一次向右走, cost1 = c[1]*len1 + c[3]*len3 + ... (len1+len3+... = n);cost2 = c[2]*len2 + c[4]*len4 + ... (len2+len4+... = n)。最后总的代价为cost1+cost2。cost1和cost2的最小化贪心可以分开考虑,两组一样操作:使c最小的值的len取最大,其他len都只取1即可。cost1/2 = sum(c[1..k])+c[min]*(n-cnt)
每次k+1后,迭代可以依赖于上一次提供的变量,这样才能不超时。
这题一步步分解下来比较简单,但要在比赛的短时间内耐心考虑清楚其实也不是件容易的事。
正常考虑到对k遍历的话肯定觉得会超时,但事实上用了点dp的思想后,只需要O(n)。
所以虽然说是暴力算法,但还是要分析清楚了。
至此,昨天做的八道题已经全部梳理了一遍,自己独立写了一遍。
这是第一篇cf刷题笔记,继续加油!
浙公网安备 33010602011771号