【重启训练】codeforces712 div2 部分题解及思考

文章说明

这场比赛我并没有打。我原本信誓旦旦地和同学说我要打,劝得他也打,结果自己睡过了,他rating掉了(笑哭)
这场比赛的题解可以从很多地方找到,如比赛announcement里更新的题解,和一个B站up的视频,直接B站上搜codeforces712就能看到。
而我写了这篇题解,主要的目的是为了自己思考与提升。我发现同学做过的题都能记住,而我却忘得一点都不剩了(无奈),就想用这种方法增加印象。
当然,我会像普通题解一样尽力把题写明白。所以如果有幸被您看到,应该能让您看懂做法。

下面是C~F,比赛链接:https://codeforces.com/contest/1504

C. Balance the Bits

题目就自己看好吗。

通过这题,我发现意外的灵感其实蛮重要的,甚至是比赛的关键。因此,以一个精神饱满的面貌迎接比赛是很有必要的。

一些输出YES的条件其实很容易发现:

  1. 首尾必须是1(第一个一定是左括号,最后一个必须是右括号)
  2. 0的个数必须是偶数个,且0的位置左括号数必须等于右括号数(整个括号序列左括号和右括号数目是固定的,如果0的位置中左括号和右括号数目不同,那么变换后左右括号数目将发生改变)

这两个性质我认为一般人都会想到。那么接下来,我们面临着两个问题:

  1. 满足上述条件的一定能构成合法序列吗?
  2. 怎么构建合法序列?

也许这两个问题可以同时解决,也就是在那两个条件下找到一个构造法。

我们可以考虑括号序列的固有性质:对于一个右括号,存在与他配对的左括号的条件是,他的左边有数量足够多的左括号,而左括号的具体位置并不重要。
对于每一个需要变的左括号,它能变成右括号的条件是,他的左边有一个能与他匹配的左括号。
对于每个需要变得右括号,变化前,他需要一个与之匹配的左括号。
于是诞生一个想法,先把所有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. 这个路线是一个环我们容易看出,不论从这个环的哪个点开始旅行,结果都是一样的,也就是说解决问题时不必以1为起点。
  2. 上述我们的算法中,可以把这趟旅程,看成了从最小的数a_1,到最大的数a_n,需要的最小费用。如果直接从a_1到a_n,需要的费用是a_n-a_1。但我们有一些捷径:从各个a_i到a_i+c_i是没有花费的。我们就是要利用这些捷径,缩短费用。这也就是上述代码所表达的东西。
  3. 由于我们按从大到小排的序,因此从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);
}
posted @ 2021-04-07 16:30  white514  阅读(71)  评论(0编辑  收藏  举报