Codeforces Global Round 9 解题报告

CF1375A Sign Flipping

非常简单的一道构造题。

首先, n n n 必定为奇数,那么 n − 1 n-1 n1 必定为偶数,即相邻两数差的个数也是偶数。

题目里说了至少有 n − 1 2 \dfrac{n-1}{2} 2n1 个差为非负,也至少有 n − 1 2 \dfrac{n-1}{2} 2n1 个差为非正。

注意到相邻两数计算的方法为 a i + 1 − a i a_{i+1}-a_i ai+1ai,那么不难想到我们只要让 a i a_i ai 的符号按“正、负、正、负、正……”的顺序排列就可以了。这样一来,差的正负就与两数绝对值的大小关系无关了,差一定是按照“负、正、负、正、负……”的顺序排列的,满足题目要求。

尽管 0 0 0 既不是正数也不是负数,但是我们不需要考虑 0 0 0

上代码:

#include<iostream>
#include<cmath>
using namespace std;

void solve()
{
	int n;
	cin >> n;
	for(int i = 1; i <= n; i++)
	{
	    int x;
	    cin >> x;
	    if(i & 1)
	        cout << abs(x) << ' ';
	    else
	        cout << -abs(x) << ' ';
	}
	cout << endl; 
	return;
}

int main()
{
	int T;
	cin >> T;
	while(T--)
		solve();
	return 0;
}

CF1375B Neighbor Grid

又是一道构造题。

我们可以先考虑如何判断答案为 NO \texttt{NO} NO

角上的格子周围最多有 2 2 2 个格子中的数大于 0 0 0,边上的格子周围最多有 3 3 3 个格子中的数大于 0 0 0,中部的格子周围最多有 4 4 4 个格子中的数大于 0 0 0。所以如果题目给出的 a i , j a_{i,j} ai,j 大于上面所述的上限,那么自然无法构造,因为我们只能增加格子中的数。

接着构造的方法就出来了——让每个格子的数都达到其上限。换言之,把每个格子都填满。

这里的代码我将角、边和中部分开处理了。

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 310;
int a[maxn][maxn];

void solve()
{
	int n, m;
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			cin >> a[i][j];
	bool flag = true;
	//角
	if(a[1][1] > 2 || a[1][m] > 2 || a[n][1] > 2 || a[n][m] > 2)
		flag = false;
	//边
	for(int j = 2; j < m; j++)
		if(a[1][j] > 3 || a[n][j] > 3)
			flag = false;
	for(int i = 2; i < n; i++)
		if(a[i][1] > 3 || a[i][m] > 3)
			flag = false;
	//中部
	for(int i = 2; i < n; i++)
		for(int j = 2; j < m; j++)
			if(a[i][j] > 4)
				flag = false;
	if(!flag)
	{
		puts("NO");
		return;	
	}
	puts("YES");
	//和上边判断的过程同理
	a[1][1] = a[1][m] = a[n][1] = a[n][m] = 2;
	for(int j = 2; j < m; j++)
		a[1][j] = a[n][j] = 3;
	for(int i = 2; i < n; i++)
		a[i][1] = a[i][m] = 3;
	for(int i = 2; i < n; i++)
		for(int j = 2; j < m; j++)
			a[i][j] = 4;
	for(int i = 1; i <= n; i++)
	{
		for(int j = 1; j <= m; j++)
			cout << a[i][j] << ' ';
		cout << endl;
	}		 
	return;
}

int main()
{
	int T;
	cin >> T;
	while(T--)
		solve();
	return 0;
}

CF1375C Element Extermination

当我们想不出如何才能判断答案为 YES \texttt{YES} YES 的时候,不妨从对立面想,看看什么时候答案为 NO \texttt{NO} NO

结合题目中的规则,以及样例可以观察到,在数组变化的过程中,最左端的数的大小一定不会减小,最右端的数的大小一定不会增加。

证明:移除一个数的条件为 a i < a i + 1 a_i < a_{i+1} ai<ai+1,如果最左端的数我们不移除,那么很显然,最左端的数大小不变;如果我们移除了最左端的数,那么新的最左端的数就成了 a i + 1 a_{i+1} ai+1,比原来的 a i a_i ai 要大。最右端的数的变化情况同理。

所以如果 a 1 > a n a_1 > a_n a1>an,不管怎么消除数,最后一定不能只剩下 1 1 1 个数。因为就算我们能消到只剩下最后两个数,根据上面证明的结论,这两个数是一定无法移除其中一个的。

接着进行猜想:如果 a 1 < a n a_1 < a_n a1<an,那么答案为 YES \texttt{YES} YES

接下来我们就要尝试想出一个策略,保证当 a 1 < a n a_1 < a_n a1<an 时,我们一定能按照规则操作使得最后只剩下一个数。

这个策略是这样的:每次找到一个离 a 1 a_1 a1 最近的一个数 a x a_x ax,满足 a x > a 1 a_x > a_1 ax>a1,然后我们用这个 a x a_x ax 一路向 a 1 a_1 a1 “推”,“挤”掉所有下标在 ( 1 , x ) (1,x) (1,x) 中的数 a i a_i ai,然后这个 a x a_x ax 就来到了 a 1 a_1 a1 旁边,我们再把 a x a_x ax 移走。反复重复这个策略,最后成为 a x a_x ax 的一定是 a n a_n an,最后数组只剩下了 a 1 a_1 a1

综上所述:

  • a 1 > a n a_1 > a_n a1>an 时,不可能做到使数组只剩下一个数;
  • 否则,则必定可以使数组只剩下一个数。

代码:

#include<cstdio>
const int maxn = 3e5 + 10;
int a[maxn];

void solve()
{
	int n;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
		scanf("%d", a + i);
	puts(a[n] > a[1] ? "YES" : "NO");
	return;
}

int main()
{
	int T;
	scanf("%d", &T);
	while(T--)
		solve();
	return 0;
}

CF1375D Replace by MEX

读题时看到了这段话:

Please note that you do not have to minimize the number of operations. If there are many solutions, you can print any of them.

可见是构造题无疑了。

也不知道为什么,经过尝试后就想到将数组变成 [ 0 , 1 , 2 , 3 , … , n − 1 ] [0,1,2,3,\dots,n-1] [0,1,2,3,,n1] 的形式。

接下来我们想办法构造:
注:接下来的数组下标全部从 1 1 1 开始。

  1. 如果当前某个数 a i = i − 1 a_i=i-1 ai=i1,我们后面就不要修改了;
  2. 如果当前 MEX ⁡ = n \operatorname{MEX}= n MEX=n,我们将任何一个不满足 a i = i − 1 a_i=i-1 ai=i1 a i a_i ai 替换为 MEX ⁡ \operatorname{MEX} MEX
  3. 如果当前 MEX ⁡ ≠ n \operatorname{MEX}\neq n MEX=n,我们将 a MEX ⁡ + 1 a_{\operatorname{MEX}+1} aMEX+1 替换为 MEX ⁡ \operatorname{MEX} MEX

首先,每进行一次操作 3 3 3,就会固定一个 a i a_i ai 的值,即 a i a_i ai 不会再被操作。这是因为在该操作后, MEX ⁡ \operatorname{MEX} MEX 永远不可能等于 a i a_i ai,原因是 MEX ⁡ \operatorname{MEX} MEX 的定义。

其次,每个数最多被进行一次操作 2 2 2,因为如果我们进行了操作 2 2 2,那么 n n n 在数组中没有出现过,进行过操作 2 2 2 MEX ⁡ \operatorname{MEX} MEX 不可能为 n n n,在操作 3 3 3 进行在该数上之前,此数不可能被再次替换。

综上,每个数最多被操作两次(变为 n n n 后再变为 i − 1 i-1 i1),所以总操作个数最大为 2 n 2n 2n,满足题目要求。

重复若干次如上所述的操作,直到数组变成 [ 0 , 1 , 2 , 3 , … , n − 1 ] [0,1,2,3,\dots,n-1] [0,1,2,3,,n1] 即可。

由于数据范围比较小,所以很暴力地求 MEX ⁡ \operatorname{MEX} MEX 即可。

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn = 1010;
int a[maxn], cnt, n;
bool vis[maxn], mark[maxn];
vector<int> op;

int main()
{
	int T;
	cin >> T;
	while(T--)
	{
		cin >> n;
		fill(mark + 1, mark + n + 1, false);
		op.clear();
		cnt = n;
		for(int i = 1; i <= n; i++)
		{
			cin >> a[i];
			if(a[i] == i - 1)
			{
				mark[i] = true;
				cnt--; 
			} 
		}
		while(cnt)
		{
			int mex;
			fill(vis, vis + n + 1, false);
			for(int i = 1; i <= n; i++)
				vis[a[i]] = true;
			for(int i = 0; i <= n; i++)
				if(!vis[i])
				{
					mex = i;
					break;
				}
			if(mex == n)
			{
				for(int i = 1; i <= n; i++)
					if(!mark[i])
					{
						op.push_back(i);
						a[i] = mex;
						break;
					}
			}
			else
			{
				op.push_back(mex + 1);
				a[mex + 1] = mex;
				mark[mex + 1] = true;
				cnt--;
			}
		}
		cout << op.size() << endl;
		for(int i : op)
			cout << i << ' ';
		cout << endl;
	}
	return 0;
}

CF1375E Inversion Swapsort

为了便于阅读,以下部分变量的下标用代码中数组的形式代替。

题目本质上就是让我们将所有逆序对的下标 ( u , v ) (u, v) (u,v) 进行一个排列,使得我们依次交换 a u a_u au a v a_v av 后, a a a 成为一个不下降序列。

首先我们发现,因为我们的题目需要的是逆序对的下标,所以只要我们保证我们没有更改原数组中的逆序对下标,我们可以任意修改所有数的值。

这样一来,我们就可以将任意数据转化为一个排列以简化问题。转化的具体方式可以看代码理解。接下来我们只需要考虑怎么将一个排列按照要求排序就可以了。

此题数据范围 1 ≤ n ≤ 1000 1 \le n \le 1000 1n1000,这意味着我们可以用 O ( n 2 ) O(n^2) O(n2) 的时间复杂度通过本题。

仿效冒泡排序和选择排序的思想,通过一系列步骤,将区间最小值放到排序区间的左端,或将区间最大值放到排序区间的右端,然后对一个更小的区间重复相同的操作,最后达到使数组有序的目的。于是我们也考虑每次将区间最大值放到排序区间的最右端。

但是要想这么做,我们必须满足一些规则:
设当前排列为 a a a,进行一次操作后的排列为 p p p,那么:

  1. p n = n p_n=n pn=n
  2. 对于任意的 1 ≤ i , j < n 1 \le i,j \lt n 1i,j<n p i p_i pi p j p_j pj 的大小关系与 a i a_i ai a j a_j aj 的大小关系相同

规则 1 1 1 就是为了满足我们的目的,规则 2 2 2 保证我们的操作满足题目所给的条件——我们交换的逆序对一定对应原序列的一个逆序对。

接下来这题又变成了一个构造题:构造一个算法使得我们满足上述规则并且恰好用完所有的原序列中的逆序对。

算法是这样的:
p o s [ i ] pos[i] pos[i] 表示数 i i i 所在的位置,我们依次交换 ( p o s [ a n + 1 ] , n ) , ( p o s [ a n + 2 ] , n ) , ( p o s [ a n + 3 ] , n ) , … , ( p o s [ n ] , n ) (pos[a_n+1],n),(pos[a_n+2],n),(pos[a_n+3],n),\dots,(pos[n],n) (pos[an+1],n),(pos[an+2],n),(pos[an+3],n),,(pos[n],n)。(这里括号内的都是数组下标)

执行完一次该算法后,我们满足了规则 1 1 1,因为 n n n 被成功放到了最后;我们满足了规则 2 2 2,因为我们把 a [ p o s [ a n + k ] ] ( 1 ≤ k < n − a n ) a[pos[a_n+k]](1 \le k \lt n-a_n) a[pos[an+k]](1k<nan) 全部减了 1 1 1,大小关系自然不变;我们还用完了原序列中所有第二下标为 n n n 的逆序对。

反复重复这个操作即可。

emmm……好像并没有无解的情况。

时间复杂度 O ( n 2 ) O(n^2) O(n2)

#include<iostream>
#include<utility>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn = 1005;
struct element
{
	int val;
	int idx;
} p[maxn];
int a[maxn], pos[maxn];
vector<pair<int, int> > res;

bool cmp(element x, element y)
{
	if(x.val != y.val)
		return x.val < y.val;
	return x.idx < y.idx;  //注意下标的先后顺序
}

int main()
{
	int n;
	cin >> n;
	for(int i = 1; i <= n; i++)
	{
		cin >> p[i].val;
		p[i].idx = i;
	}
	sort(p + 1, p + n + 1, cmp);//以数值和下标为关键字排序
	for(int i = 1; i <= n; i++)
		p[i].val = p[i - 1].val + 1;
	for(int i = 1; i <= n; i++)
		a[p[i].idx] = p[i].val;//成功转化成排列
	for(int i = 1; i <= n; i++)
		pos[a[i]] = i;
	for(int i = n; i >= 1; i--)
	{
		for(int j = 1; j <= i - a[i]; j++)
		{
			res.push_back(make_pair(pos[a[i] + j], i));
			a[pos[a[i] + j]]--;
		}
		for(int j = 0; j < i - a[i]; j++)
			pos[a[i] + j] = pos[a[i] + j + 1];
		pos[i] = i;
	}
	cout << res.size() << endl;
	for(pair<int, int> i : res)
		cout << i.first << ' ' << i.second << endl;
	return 0;
}

CF1375F Integer Game

又是一道构造题……

读完题目看样例,似乎发现了什么不得了的东西。

思考为什么样例里最后先手赢了:

  • 最后一步,石子数变为 5 , 2 , 8 5,2,8 5,2,8,先手给了 3 3 3 颗石子,因为 8 − 5 = 5 − 2 = 3 8-5=5-2=3 85=52=3,所以后手只要放在第一堆或第二堆上都必输。
  • 另外上一步中 8 8 8 被操作过了,所以可怜的 Anton 不能继续在第三堆石子上继续放石子,只能认输。

这给了我们启示:当这三个数构成等差数列,且最大数不能操作时,后手必败

于是我们充当先手,通过构造方案击败后手。我们的目标是,通过添加石子,让三个数构成等差数列并且操作的数是等差数列中最大的那个。

设三个数分别为 a , b , c a,b,c a,b,c,且满足 a < b < c a < b < c a<b<c。分类讨论:

设给后手的数为 x x x

  1. 若后手将数添加在了 a a a 上,那么由题意得, a + x + b = 2 c a+x+b=2c a+x+b=2c,解得 x = 2 c − a − b x=2c-a-b x=2cab
  2. 若后手将数添加在了 b b b 上,那么由题意得, b + x + a = 2 c b+x+a=2c b+x+a=2c,解得 x = 2 c − a − b x=2c-a-b x=2cab
  3. 若后手将数添加在了 c c c 上……很遗憾,我们不一定能满足条件。

对策略进行调整:如果后手将石子添加在了较小的两堆上,那么先手必胜。

既然这样,为什么我们不再加一步操作,使后手无法操作最大的那一堆呢?

于是我们可以添加一个很大的数,比如 1 0 11 10^{11} 1011,这样不管哪一堆添加过后都会成为最大数。

注意这个数不要太大,不然会超出 y ≤ 1 0 12 y \le 10^{12} y1012 的限制。

之后我们再给后手一个等差数列的公差,后手必败。

于是……这道题变成了顺序结构的程序题?!

#include<cstdio>
typedef long long ll; 

void write(ll x)
{
	if(x == 0)
		return;
	write(x / 10);
	putchar(x % 10 + 48);
	return;
}

signed main()
{
	ll a[4];
	scanf("%lld%lld%lld", a + 1, a + 2, a + 3);
	puts("First");
	fflush(stdout);
	//加上一个特别大的数,使最大的一堆无法操作
	write(1e11);
	putchar('\n');
	fflush(stdout);

	int pile;
	scanf("%d", &pile);
	int mxp = pile;
	a[mxp] += 1e11;
	//使三个数成为等差数列并且让操作的一堆最大
	write(3 * a[mxp] - a[1] - a[2] - a[3]);
	putchar('\n');
	fflush(stdout);	

	scanf("%d", &pile);
	int x = pile;
	//加上公差,后手必败
	write(a[mxp] - a[6 - x - mxp]);
	putchar('\n');
	fflush(stdout);
	
	scanf("%d", &pile);
	//胜利的曙光
	return 0;
}

CF1375G Tree Modificaion

这道题问我们最少通过多少步把普通的树变成菊花图,找不到什么比较好的算法(反正我是没找到),于是不得不分析题目里一连串复杂操作的实质。

树是一个二分图,也就是说,我们可以对树进行黑白染色,使得一个黑色节点周围所有与它相邻的节点都是白色节点,反之亦然。而菊花图进行黑白染色过后,其中有一个颜色只出现了 1 1 1 次,所以我们的目标就是让其中一个颜色出现的次数变为 1 1 1

逐一分析题目中的操作:

操作 1 1 1 中我们选择了三个相邻的节点 a , b , c a,b,c a,b,c,因为 b b b a , c a,c a,c 相邻,所以不妨设 b b b 为黑色, a , c a,c a,c 为白色。

操作 2 2 2 中,所有除 b b b 外与 a a a 相邻的节点均为黑色,把它们从 a a a 上挂到 c c c 上,它们还是黑色,颜色并未改变。

操作 3 3 3 中,把 a a a b b b 挂到 c c c 上, a a a 由白色变为黑色。

所以我们一连串操作下来只改变了一个节点的颜色

那么我们首先猜想,设 c n t w cnt_w cntw 表示原树中被染成白色的节点个数, c n t b cnt_b cntb 表示原树中被染成黑色的节点个数,那么答案就是 min ⁡ { c n t w , c n t b } − 1 \min\{cnt_w, cnt_b\}-1 min{cntw,cntb}1

实际上这是正确的,因为我们每次一定能将 c n t w cnt_w cntw c n t b cnt_b cntb 其中一个减去 1 1 1。我们可以证明,只要其中一种颜色,满足被染成这种颜色的节点数大于 1 1 1,那么必定有两个节点被染成这种颜色,且存在另一个不同色的节点与它们都相邻,那么我们就可以通过题目中的操作将这种颜色所对应的 c n t cnt cnt 减去 1 1 1

最后就是一个 DFS 黑白染色的事了。

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 2e5 + 10;
vector<int> g[maxn];
int n, cnt[2];

void dfs(int x, int fa, int col)
{
	cnt[col]++;
	for(int v : g[x])
		if(v != fa)
			dfs(v, x, col ^ 1);
	return;
}

int main()
{
	scanf("%d", &n);
	for(int i = 1; i < n; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1, 0, 0);
	printf("%d\n", min(cnt[0], cnt[1]) - 1);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值