【重启训练】codeforces712 div2 部分题解及思考
文章说明
这场比赛我并没有打。我原本信誓旦旦地和同学说我要打,劝得他也打,结果自己睡过了,他rating掉了(笑哭)
这场比赛的题解可以从很多地方找到,如比赛announcement里更新的题解,和一个B站up的视频,直接B站上搜codeforces712就能看到。
而我写了这篇题解,主要的目的是为了自己思考与提升。我发现同学做过的题都能记住,而我却忘得一点都不剩了(无奈),就想用这种方法增加印象。
当然,我会像普通题解一样尽力把题写明白。所以如果有幸被您看到,应该能让您看懂做法。
下面是C~F,比赛链接:https://codeforces.com/contest/1504
C. Balance the Bits
题目就自己看好吗。
通过这题,我发现意外的灵感其实蛮重要的,甚至是比赛的关键。因此,以一个精神饱满的面貌迎接比赛是很有必要的。
一些输出YES的条件其实很容易发现:
- 首尾必须是1(第一个一定是左括号,最后一个必须是右括号)
- 0的个数必须是偶数个,且0的位置左括号数必须等于右括号数(整个括号序列左括号和右括号数目是固定的,如果0的位置中左括号和右括号数目不同,那么变换后左右括号数目将发生改变)
这两个性质我认为一般人都会想到。那么接下来,我们面临着两个问题:
- 满足上述条件的一定能构成合法序列吗?
- 怎么构建合法序列?
也许这两个问题可以同时解决,也就是在那两个条件下找到一个构造法。
我们可以考虑括号序列的固有性质:对于一个右括号,存在与他配对的左括号的条件是,他的左边有数量足够多的左括号,而左括号的具体位置并不重要。
对于每一个需要变的左括号,它能变成右括号的条件是,他的左边有一个能与他匹配的左括号。
对于每个需要变得右括号,变化前,他需要一个与之匹配的左括号。
于是诞生一个想法,先把所有0的位置找出,一次填上左括号、右括号、左括号、右括号...,那么第一个括号序列成立的话,第二个括号序列第一个0的位置是右括号,正好与序列开头的左括号匹配,后面0位置的括号也容易看出都有匹配。这样的话,剩下1位置的括号,是不会变化的,因此只要让他们原本就匹配就好了。
我的代码就是这么写的。
回顾一下,括号序列问题,主要特点就是 对应,只要为每个左括号找到对应的右括号(或相反)就行了。而括号序列的对应条件是很宽松的,也就是左括号对应的右括号只要在他右边就可以,而具体位置不重要。
比赛时抓住这个特征想,或许就容易想到。由知道特点到想出方法的过程,也许真的需要一点运气吧。
#include<bits/stdc++.h>
using namespace std;
const int N=200000+10;
int t, n;
int a[N];
int res1[N], res2[N];
vector<int> v;
bool vis[N];
void print(int *res){
for(int i=1; i<=n; ++i){
if(res[i]==0) putchar('(');
else putchar(')');
}
puts("");
}
int main(){
cin>>t;
while(t--){
scanf("%d", &n);
v.clear();
for(int i=1; i<=n; ++i){
scanf("%1d", a+i);
res1[i]=res2[i]=0;
vis[i]=false;
if(a[i]==0){
v.push_back(i);
}
}
int sz=v.size();
if(sz&1 || (sz && v[0]==1) || sz&&v[sz-1]==n){
puts("NO");
continue;
}
res1[1]=0; res1[n]=1;
vis[1]=vis[n]=true;
for(int i=0; i<sz; ++i){
res1[v[i]]=i&1;
vis[v[i]]=true;
}
int cnt=0;
for(int i=1; i<=n; ++i){
if(vis[i]) continue;
if(cnt&1){
res1[i]=1;
}else{
res1[i]=0;
}
cnt++;
}
for(int i=1; i<=n; ++i){
res2[i]=a[i]==0?1-res1[i]:res1[i];
}
puts("YES");
print(res1);
print(res2);
}
}
D. 3-Coloring
按题目的意思,是无论先手给出什么样的颜色,后手都能给出方案,并走向胜利。
我的想法是先否定掉会存在“接头(动词)”情况的方案;于是开始思考从边角一圈一圈地扩展的方案,但如果碰到一个块它的两个邻块颜色不同的情况,就失败了。
没错,这个很重要,一定要防止“一个还没涂色的块它的两个邻块颜色不同”。因为这样他的颜色就是唯一的,如果一直不能填这个颜色,那么就失败了。
那么斜着涂就好了。
为了好表示,我们用斜向右下的斜线,也就是同一条写线上横纵坐标r+c为定值的斜线。若r+c为偶数,这里简称偶斜线,否则为奇斜线。
如果所有奇斜线都是用一种颜色,或所有偶斜线都是同一种颜色,比如填的是2,那么剩下的方块随便填1和3都可以。
能不能做到这一点呢?
设想,如果先手给出1或3,那我们就将2填到偶斜线上;如果先手给出2,那我们就将1填到奇斜线上。如果偶斜线先填满了,那我们只要继续在奇斜线上填1或3就好了;如果奇斜线先填满了,此时奇斜线一定填的都是1,那么我们就继续在偶斜线上填2或3就好了。
以上就是这题的思路。而那位B站up主又提到了另一道题:
给定一个n*m的矩阵,可以将每个数各自-1或+1,使得矩阵上相邻两数不相等。这里“相邻”的定义与此题相同。
可以看出这两题很相似:都要求相邻不相等。
这道题的做法是“奇偶划分”,将奇斜线和偶斜线上分别全控制为奇数和偶数。这两题异曲同工,尤其是“奇偶划分”这个想法非常巧妙。
#include<bits/stdc++.h>
using namespace std;
int n;
vector<pair<int,int>> v[2];
int main(){
scanf("%d", &n);
for(int i=1; i<=n; ++i){
for(int j=1; j<=n; ++j){
v[(i+j)&1].push_back(pair<int,int>(i,j));
}
}
int sz0=v[0].size(), sz1=v[1].size();
int cur0=0, cur1=0;
for(int i=1, x; i<=n*n; ++i){
scanf("%d", &x);fflush(stdout);
if(cur0<sz0 && x!=2){
printf("2 %d %d\n", v[0][cur0].first, v[0][cur0].second);
cur0++;
}else if(cur1<sz1){
printf("%d %d %d\n", x==2||x==1?3:1, v[1][cur1].first, v[1][cur1].second);
cur1++;
}else{
printf("1 %d %d\n", v[0][cur0].first, v[0][cur0].second);
cur0++;
}
fflush(stdout);
}
}
E. Travelling Salesman Problem
这题我没做出来,不过现在觉得没想出来真的是心理对题目的畏惧导致的。我读题之前,就已经看到了*2200和dp的标签,这对我后面的思考有很大影响。这道题,比想象中的要简单。
我看了rank1 aijmas的代码,觉得他的正符合我想。
我们看数据范围n为10^5级别,可以判断是O(n)或O(nlogn)的算法。读题后,可以知道重要的是寻找到一个顺序,寻找的方法很可能就是排序。
前一天还在看一道邻项交换排序的题目,因此我自然而然地往那个方向想去。然而,列出的不等式中有左右各有3个max函数,这我无法简化,只能放弃。
这道题c是不可避免的花费,我们只需要算出多出c的那部分即可。可排序似乎也不能仅仅依靠a排(然而确实可以仅仅依靠a排!)
这个max很讨厌,我很想消掉它。容易想到max(c_i,a_j-a_i)等价于c_i+max(0,a_j-(a_i+c_i)),然而这么化简后max还是存在的,只是比较的对象变成了0。似乎变简单了,但我还是无法忽视它。我止步于此。
但是,看到aijmas的代码后,我顿悟:消除max的方式是排序。
我们可以把一个城市看成两个数字a_i和a_i+c_i,分别是进入时的数字和离开时的数字。换句话说,如果要从城市j进入城市i,那么花费是max(0,a_i-(a_j+c_j));如果是从i进入j,那么花费是max(0,a_j-(a_i+c_i))。
之后,为了消除max,我们将所有城市的a_i和a_i+c_i放到一起进行排序。也就是这2*n个数字,不受是否表示同一个城市的约束进行排序。那么在这个序列中,后面的减前面的一定是大于0的,就不需要max了。
由于这道题限制c>=0,因此对于每个城市,a_i必然是排在a_i+c_i之前的。而如果存在j,使得a_j>a_i+c_i,那么直接从城市i到城市j会花费a_j-a_i+c_i;如果不想花这个钱,只能找到城市k,使得a_k<a_i+c_i并且a_k+c_k>a_j。
遵循这个思路,我们就得到如下代码,也就是aijmas的核心代码:
int cnt=0;
for(int i=1; i<dn; ++i){
if(d[i].tag){
cnt--;
if(!cnt) sum+=d[i+1].a-d[i].a;
}else{
cnt++;
}
}
//d[]含有两个属性a和tag,a是上述某个a_i或者某个a_i+c_i。若a=a_i,则tag=0;若a=a_i+c_i,则tag=1。
将原本描述同一个属主的两个数拆开来,和描述其他属主的数混合起来排序,这种方法很突破常规,相当精彩。
我们再对题目的其他性质进行分析。
- 这个路线是一个环我们容易看出,不论从这个环的哪个点开始旅行,结果都是一样的,也就是说解决问题时不必以1为起点。
- 上述我们的算法中,可以把这趟旅程,看成了从最小的数a_1,到最大的数a_n,需要的最小费用。如果直接从a_1到a_n,需要的费用是a_n-a_1。但我们有一些捷径:从各个a_i到a_i+c_i是没有花费的。我们就是要利用这些捷径,缩短费用。这也就是上述代码所表达的东西。
- 由于我们按从大到小排的序,因此从a_n+c_n到a_1是可以确定没有花费的。
这道题再次告诉我,我的思路是没问题的,只是不知道实现的方法。所以要积累经验不是吗?而且,不要被标签吓到,也要时刻怀疑标签是否在误导人。(这道题的标签还真是丰富啊...)
#include<bits/stdc++.h>
using namespace std;
const int N=100000+10;
int n;
int a[N], c[N];
struct Data{
int a, tag;
bool operator<(const Data&t)const{
return t.a!=a?a<t.a:tag<t.tag;
}
}d[N*2];
int dn;
long long sum;
int main(){
cin>>n;
for(int i=1; i<=n; ++i){
scanf("%d%d", a+i, c+i);
sum+=c[i];
d[++dn].a=a[i]; d[dn].tag=0;
d[++dn].a=a[i]+c[i]; d[dn].tag=1;
}
sort(d+1,d+dn+1);
int cnt=0;
for(int i=1; i<dn; ++i){
if(d[i].tag){
cnt--;
if(!cnt) sum+=d[i+1].a-d[i].a;
}else{
cnt++;
}
}
cout<<sum<<endl;
}
F. Flip the Cards
这道题可以变换成如何将一段序列转化为两条递减序列。我已经想到了这一步,却没想到如何解。
首先第一步:问题转化。
题解思路:对于已经排好了的一副牌,要拿当前最小的牌,一定是从正面的最左端或背面的最右端拿,这样不断抽牌,直至抽出n张牌。在这个过程中你会发现,一张牌绝不可能正反两面的数字都<=n。于是我们获得了一个判断条件。
接下来,设印有k的牌,它的另一面是f(k)。那么对于[f(1),f(2),...,f(n)]这个序列(记作F序列),它一定是由两个递减序列构成的,对吧。
我的思路:对于已经排好了的一副牌,我们画出 位置-数值 图:横坐标是位置,纵坐标是数值(注意,同一个位置有正反面两个数值),我们会发现它呈现一个x的形状。他们会有一个交叉的点,我们将图形沿着这个点所在竖线对折,将右边图形向左翻折,会发现图形分为上下两部分,上面的部分由两条递减序列组成,下面的部分由两条递增序列组成。我们依据下面的数值给位置重新排序,那么上面的部分就变成了两条递减序列。
然后第二步:如何求这两个递减序列
假设存在i,使得min{f(1),f(2),...,f(i)}>max{f(i+1),f(i+2),...,f(n)},那么我们在i和i+1之间放个隔板。我们在所有能放隔板的位置放上隔板,那么这个序列被一段一段地分开了。现在,对于各个段而言,我们要考虑选哪几个放到第一个递减序列中、或放到第二个递减序列中;但是段与段之间不会有任何约束。
进一步,我们发现,对于每一段,它已经自然而然地分成了两个递减序列,它的分法是唯一的,我们只需要考虑放到第一个递减序列里的是哪一个就好。
那么各个段的求解关系只是相加,我们贪心就可以了。
思考问题的过程,是一个不断试错的过程。一定不要死磕一种思路。第二步由于我死磕一种很麻烦的做法,想了很久最终放弃。
第二步,它将求解的过程分成了若干步,将复杂的情况化成多个简单情况再相加。这种思路很值得学习。
#include<bits/stdc++.h>
using namespace std;
const int N=200000+10;
int a[N], sign[N];
int minn[N], maxx[N];
int n;
vector<int> V;
int main(){
bool have_ans=true;
cin>>n;
for(int i=1, x, u, v; i<=n; ++i){
scanf("%d%d", &u, &v);
if((u>n&&v>n)||(u<=n&&v<=n)){
have_ans=false;
break;
}
if(u>v){
swap(u,v);
sign[u]=true;
}
a[u]=v;
}
if(!have_ans){
puts("-1");
return 0;
}
minn[1]=a[1];
for(int i=2; i<=n; ++i){
minn[i]=min(minn[i-1],a[i]);
}
maxx[n]=a[n];
for(int i=n-1; i; --i){
maxx[i]=max(maxx[i+1],a[i]);
}
V.push_back(0);
for(int i=1; i<n; ++i){
if(minn[i]>maxx[i+1]){
V.push_back(i);
}
}
V.push_back(n);
int val1, val2, sz=V.size();
for(int i=1; i<sz&&have_ans; ++i){
val1=val2=2*n;
for(int j=V[i-1]+1; j<=V[i]; ++j){
if(a[j]<=val1){
val1=a[j];
}else if(a[j]<=val2){
val2=a[j];
}else{
have_ans=false;
break;
}
}
}
if(!have_ans){
puts("-1");
return 0;
}
int ans=0;
int num1, sign1, num2, sign2, val;
for(int i=1; i<sz; ++i){
num1=sign1=num2=sign2=0; val=2*n;
for(int j=V[i-1]+1; j<=V[i]; ++j){
if(a[j]<val){
num1++;
sign1+=sign[j];
val=a[j];
}else{
num2++;
sign2+=sign[j];
}
}
ans+=min(sign1+num2-sign2,num1-sign1+sign2);
}
printf("%d\n", ans);
}