本场比赛赛时只做出两道题,A题不必多说,秒了。
对于B题,我最开始复杂度想错了,将每个测试点中的多次遍历当场O(n2)复杂度了,因此一直在想是否有O(logn)的解法,知道最后一小时才反应过来,但由于最初的写法很复杂又浪费很长时间,随后灵光乍现想到了贪心解。
然后去做C题,把C题的输出当成了最优情况下的数字求出来了,而非答案所要的次数,直到最后五分钟才发现,也是惨败了…
Codefoeces的题相比AtCoder差别还是蛮大的,前者有很多思维题
A
问题陈述
给你一个整数 n 。你需要构造一个整数数组 a1,a2,…,an 以满足以下条件:
- 从 1 到 n 的所有 i 都是 1≤ai≤2⋅n 。
- 数组的所有元素和相邻元素的和都是成对不同的。换句话说,在数字 {a1,a2,…,an,a1+a2,a2+a3,…,an−1+an} 中,不应有两个相等的数字。
分析:
本题需要构造一个数列,满足每个元素之间以及相邻两个元素的和不能重复,换个表达就是:∀i,1≤i≤n,ai∈B&ai+ai−1∈B,而B中没有重复元素。
思路:
很容易想到,对于一个奇数列1,3,5,...,2n−1,任意相邻两者之和为偶数,显然和奇数列不重复,因此奇数列符合条件,输出奇数列即可。
Code
1 2 3 4 5 6 7 8 9 10
| void sol() { int n; cin>>n; for(int i=1;i<=n;i++) { cout<<2*i-1<<' '; } cout<<'\n'; }
|
B
问题陈述
给你一个数组 a1,a2,…,an 。你最多可以对这个数组执行一次下面的操作:
- 选择一个正整数 k 以及数组 a ∗ 的子序列 b1,b2,…,bm 并将 k 加到该子序列的每个元素上,即对每个 i 执行 abi:=abi+k 操作。
您需要确定是否有可能通过最多执行一次此操作,对数组进行非递减排序。
∗ 如果从 a 中删除任意位置上的几个元素(可能是零,也可能是全部)就可以得到 b ,那么序列 b 就是序列 a 的子序列。
分析:
给定一个数列,我们需要选定其中的某些数增加k,尝试让该数列变为非严格递增or单调不减数列,如果可以则输出YES,否则输出NO。
- 由于选中某些数并将它们增加k,并不会改变它们本来的大小关系,那么很显然,将所有需要增加k的数按照原顺序放在一个数组中,该数组只能单调不减,否则输出
NO。
- 将数列分为若干个最长的连续的递增数列(可以仅含一个元素),也就是在每次出现递减的地方作分割,设 ai>ai+1,那么就在i处分割,同时右边的连续子序列的前缀需要增加至少k=ai−ai+1。
思路:
这里直接将贪心的方法:对于每处 ai>ai+1,我们记录k至少为多少,即 k=max(ai−ai+1),逆序遍历数组a,如果 ai+k<=ai+1,那么我们就令ai=ai+k。显然,最后一个元素an增加k是更优的。
在此过程中,如果存在ai>ai+1,则无法满足条件,输出NO,否则输出YES。
不严谨证明:
上述过程实际上为,对于一个元素ai,能增加k就增加。由于是倒序,遍历到ai时,我们能够保证ai为这一处可能的最大值,从而让左侧的数据更有可能实现增序列。(简单说就是要让右边的数越大,越有可能变成增序列)
为什么取最小的k:取最小的k能够保证,在尝试实现增数列的同时,最大化可增加k的ai的个数,减少增加k后大于右侧数的可能性。
复杂度O(n)
这里给出了逆序和顺序两个版本:
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| void sol() { int n; cin>>n; bool ans=1; int mink=0; for(int i=1;i<=n;i++) { cin>>a[i]; if(i==1)continue; if(!ans)continue; if(a[i-1]>a[i])mink=max(mink,a[i-1]-a[i]); } a[n+1]=MAX; for(int i=n;i>=1;i--) { if(a[i]+mink<=a[i+1]) { a[i]+=mink; } if(a[i]>a[i+1]) { ans=0; break; } } if(ans)cout<<"YES\n"; else cout<<"NO\n"; }
for(int i=2;i<=n;i++) { if(a[i-1]>a[i]) { if(a[i-1]<=a[i]+mink) { a[i]+=mink; } else { ans=0; break; } } }
|
补题
C
问题陈述
在探索互联网深处时,花栗鼠西奥多发现了一个非常有趣的正整数序列,于是决定玩一玩。
在一次操作中,他选择了序列中的一个元素,并执行了以下操作:
- 如果所选元素是偶数,它就除以 2。
- 如果选择的元素是奇数,它就将其增加 1。
西奥非常喜欢平等,所以他想让数列中的所有数字都相等(否则,有些数字可能会感到不舒服)。因为他需要计划午餐时间,所以帮他确定使所有数字相等所需的最少运算次数。
分析:
对于一个数x进行以下操作:
目的是让数列中所有数都相等,求最小操作次数。
思路:
显然这个数字最终的路径一定是... -> 2 -> 1 ,而且变化的路径是固定的。
我们对于每个数ai,将它变成1的路径放入Si序列中,设Si长度为Li,则操作次数为Li−1。
我们将所有Si相同的后缀去掉,因为这些去掉的数就是重复路径,那么我们只需要留下去掉的序列中最前面的数即可,然后就可以统计答案了。
- 设去掉的重复路径长度为x,则ans=(∑L)−x×n。
这时我们提交会发现WA了…
!!!由于1过于特殊,它的变化为1 -> 2 -> 1 -> ...,我们单独讨论数组a中含有1的情况。对于含有1的数组,最终变成的相同数一定是1或者2。
- 如果变成1,那么ans=∑Li。
- 如果变成2,那么 ans=(∑Li)−1×n+2×sum1 。
对于第二条的解释:我们将所有Si最后的1去掉,减少一次操作,得到(∑Li)−n,但对于ai=1处,我们应该增加一次操作,而不是减少一次操作,因此需要额外补充两倍的1的个数,即sumi。
复杂度略大于O(nlogamax)
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| void sol() { int ans1=0,sum1=0; int n; cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; if(a[i]==1) { sum1++; } cnt[i]=0; while(a[i]>1) { lst[i][++cnt[i]]=a[i]; if(a[i]%2) { a[i]++; } else a[i]>>=1; ans1++; } lst[i][++cnt[i]]=1; } if(sum1) { ans1=min(ans1,ans1-n+2*sum1); cout<<ans1<<'\n'; return; } int x=0; while(cnt[1]) { int sm=lst[1][cnt[1]]; cnt[1]--; bool fg=0; for(int i=2;i<=n;i++) { if(sm!=lst[i][cnt[i]]) { fg=1; break; } cnt[i]--; if(cnt[i]<0) { fg=1; break; } } if(fg)break; x++; } x--; ans1=ans1-x*n; cout<<ans1<<'\n'; }
|