AtCoder Regular Contest 112 A~D题解

本场链接:AtCoder Regular Contest 112

A - B = C

题目大意:求\(A,B,C\)\([L,R]\)范围之内且\(A - B = C\)的方案个数.

数据范围:

\(0 \leq L \leq R \leq 10^6\)

\(1 \leq T \leq 10^4\)

思路

三个数考虑枚举其中一个数的大小:有\(A=B+C\)则枚举\(B\in[L,R]\),所以\(B+C\in[L + B,R + B]\),那么事实上能拼凑出合理的\(A\in[L + B,R]\).对于每个\(B\)都有\(R - L- B+1\)个合法的\(A\)对应.但是直接枚举所有\(B\)的取值是不合理的,时间复杂度不允许.考虑直接对之求和:下界\(B\)\(L\)显然,上界直到\(R-L-B+1\)\(0\)\(B=R - L +1\)即可.常数项和\(B\)分开求和累起来就可以了.

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	


int main() 
{
	int T;scanf("%d",&T);
	while(T--)
	{
		ll l,r;scanf("%lld%lld",&l,&r);
		if(r - l + 1 <= l)
		{
			puts("0");
			continue;
		}
		ll res = (r - 2 * l + 2) * (r - l + 1);
		res -= (r + 1) * (r - 2 * l + 2) / 2;
		printf("%lld\n",res);
	}
	return 0;
}

B - -- - B

题目大意:一开始有个数\(B\),和\(C\)块钱,花费一块钱可以让\(B\)乘上\(-1\),花费两块钱可以让\(B-1\).求最多花费\(C\)块钱的前提下能出现多少种\(B\)的取值.

数据范围:

\(-10^{18} \leq B \leq 10^{18}\)

\(1 \leq C \leq 10^{18}\)

思路

尝试dp并且矩阵快速幂,可以发现转移也没啥办法.那么剩下只有讨论数学解法了:

不妨考虑一个子问题:恰好花费\(C\)的时候,能得到的取值方案数是多少.可以注意到花费为\(1\)让之乘\(-1\)的操作对于拓宽方案来说没有很大意义,重点考虑花费\(2\)能让他减一的操作上,可以根据钱的奇偶性讨论:

  • \(C\)为奇数,那么\(C\)最多可以使用\(n = C / 2(↓)\)\(-1\)操作,并且还需要使用一次乘\(-1\)操作.为了求方案数,套路是讨论极值的范围以及中间的取值是否都能取到:首先极值就是尽量减一,再通过乘\(-1\)操作让他成最大或者最小值;其次是中间的部分由于操作是\(-1\)所以取值也是连续的,只需要把某些操作替换成两次没有意义的乘\(-1\)操作就可以了.在这种情况下:先\(-n\)再乘\(-1\)可以得到最大值\(n-B\).反过来可以在最开始就乘\(-1\)\(-n\)这样取值就是\(-B-n\)了.
  • \(C\)为偶数,那么此时有两种情况:要么不使用乘\(-1\)操作,要么用两次.对于前一种操作来说,极值是\(B-n\).对于后者来说,极值是\(n - (B - 1)\)\(-B - (n - 1)\).两者合并即可.

现在考虑原来的问题:至多是\(C\)花费的方案数,首先方案这么多不可能一个一个去冗斥,这里有个非常巧妙的性质:对花费\(C=k + 2\)能取到的方案数是完全包含\(C=k\)能取到的方案数.所以只需要求出最大的两种:\(C\)\(C-1\)的答案并且做一个简单的冗斥就可以了.

设两个恰好花费的得到的区间分别是\((a,b)\)\((c,d)\),那么只需要两个直接加起来并且去掉交集就可以了.

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	

void solve(ll B,ll C,ll& a,ll& b)
{
	if(C & 1)
	{
		a = -B - C / 2;
		b = C / 2 - B;
	}
	else
	{
		a = B - C / 2;
		b = B + (C / 2 - 1);
	}
}

int main() 
{
	ll B,C;cin >> B >> C;
	if(C == 1)
	{
		if(B == 0)	cout << "1";
		else cout << "2";
		return 0;
	}
	
	ll a,b,c,d;
	solve(B,C,a,b);
	solve(B,C - 1,c,d);
	cout << max(0ll,b - a + 1) + max(0ll,d - c + 1) - max(0ll,min(b,d) - max(a,c) + 1);
	
	return 0;
}

C - DFS Game

题目大意:给定一个\(n\)点有根树,根固定为\(1\).一开始有一个棋子在根位置上,对于每个节点一开始都有一个硬币.两个玩家分别执行如下的过程:

  • 如果当前棋子所在的位置上有硬币,收走硬币并结束回合.
  • 如果当前棋子的位置上没有硬币,找一个有硬币的儿子节点并使棋子走到儿子节点上结束回合.
  • 如果没有任何儿子节点上有硬币,那么退回到这个节点的父节点,如果是根节点则结束游戏,否则继续,只是结束回合.

求先手最多能拿到多少个硬币.

数据范围:

\(1 \leq n \leq 10^5\)

思路

树形问题的一个基本套路:可以把当前节点\(u\)的问题分割成若干个以子节点\(v\)为根节点的子树的子问题,之后合并若干问题得到以\(u\)为根节点的子树的子问题,最后合并到根节点得到整个问题的答案.

通过手推一些样例可以注意到一个非常关键的性质:如果从某个点往下遍历一个大小为偶数的子树,那么回到这个点的时候当前的玩家是谁并不会变化:具体来说,如果当前是根节点,在先手取完了根节点上的硬币的时候,会轮到后者选取一个儿子节点,如果后者选择一个大小为偶数的儿子节点,那么回到根节点的时候,仍然是后手来选取下一个儿子.反过来如果选择一个奇数大小的子树,那么会交换当前谁来选择儿子节点.所以一个关键就是讨论选择什么样的节点:

\(f[u]\)表示以\(u\)为根的子树中,先手减后手得到的硬币数(分开做也可以,把两个状态拼一起可能会有点麻烦).对于\(f[u]\)求解的时候,显然一开始\(f[u]=1\),因为先手会取走根节点上的硬币,现在就轮到后手来选了:把所有的儿子节点分成三类:大小为偶数并且\(f[v]<0\)的,大小为偶数并且\(f[v] \geq 0\)的,大小为奇数的.对于后手来说:当后手进入子树\(v\)的时候,对于\(f[u]\)的影响是\(f[u] += f[v]\),后手肯定要优先把\(f[u]\)搞小,对称的先手如果进入\(v\)点,因为\(f[v]\)的计算是以后手进入来计算的,所以要反过来\(f[u] -= f[v]\).对于先手选取也是选取较小的,因为减下去影响最小(对于负数来说也是最大).

那么对于后手来说肯定是贪心的先把所有大小为偶数并且\(f[v]<0\)的选走,因为偶数不会交换玩家,可以直接一次性选完.对于\(\geq 0\)的先合并一下把他们的总权值\(sum\)放在一边.接下来考虑奇数大小的以及\(sum\)的分配问题:通过上面分析可以发现正反手都是希望值越小越好,那么直接按从小到大排序就可以了,对于\(sum\)这部分来说,由于他不交换玩家,所以要选只能全选完.对于奇数大小的子树的选取,首先由后手执行,每次后手是加正手是减,那么正手就不可能在这个过程中去把\(sum\)权值算进去,因为正手去选择\(sum\)的时候是\(-sum\)的,这不满足最大,所以能只能是:后手别无选择了,走到最后奇数大小的全部选完了恰好轮到反手来选最后一个\(sum\)的部分.否则是正手把最后的\(sum\)减掉.

具体实现的时候可以直接把\(sum\)放到排序好的数组末尾,轮到谁就是谁.

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	

const int N = 1e5+7,M = 2 * N;
int edge[M],succ[M],ver[N],idx;
int n,siz[N],f[N];

void add(int u,int v)
{
	edge[idx] = v;
	succ[idx] = ver[u];
	ver[u] = idx++;
}

void dfs(int u,int fa = -1)
{
	f[u] = 1;siz[u] = 1;vector<int> seq;int sum = 0;
	for(int i = ver[u];~i;i = succ[i])
	{
		int v = edge[i];
		if(v == fa)	continue;
		dfs(v,u);
		siz[u] += siz[v];
		if(siz[v] % 2 == 1)	seq.push_back(f[v]);
		else
		{
			if(f[v] < 0)	f[u] += f[v];
			else sum += f[v];
		}
	}
	sort(seq.begin(),seq.end());
	seq.push_back(sum);
	for(int i = 0;i < seq.size();++i)
	{
		if(i % 2 == 0)	f[u] += seq[i];
		else f[u] -= seq[i];
	}
}

int main() 
{
	memset(ver,-1,sizeof ver);
	int x;scanf("%d",&n);
	forn(i,2,n)	scanf("%d",&x),add(i,x),add(x,i);	

	dfs(1);
	
	printf("%d",(n + f[1]) / 2);
	return 0;
}

D - Skate

题目大意:有个\(n*m\)大小的溜冰场,标记为#的地方是平地,并且整个溜冰场外边环绕一圈墙壁.人可以选择四个方向滑动,如果走到一个平地,那么人会停留在平地上,否则会一直移动直到碰到墙为止.求最少让几个冰面变成平地,可以使人从任何一个起点出发,能经过所有点.

思路

任何一个起点出发都能经过任何点这个目标比较吊比,先不管他.通过第一个样例可以注意到一件事情:第一行第一列和第\(n\)和第\(m\)列这四个对象都是相互可达的,这个关系是一个无向的关系并且具有传递性,不妨拿并查集合并这些点.其次对于一个在\((r,c)\)位置的平地来说,他的存在可以推出一个类似的结论:\(r\)行和\(c\)列这两个对象也是互相可达的.接着通过样例一可以想到一个结论:如果所有行都是互相可达的,那么肯定可以让所有点作为起点的时候能经过所有点;对于列也是同理.所以剩下的就非常显然了:如果想要所有行都是相互可达的,只需要找出行对应的联通块的个数,加一个平地可以连接两个行,答案就是联通块数量\(-1\),对于列也是同理.

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define forn(i,x,n) for(int i = x;i <= n;++i)	

const int N = 1005;
char s[N][N];
int fa[N * 2];

int find(int x)
{
	if(x == fa[x])	return x;
	return fa[x] = find(fa[x]);
}

void merge(int x,int y)
{
	x = find(x),y = find(y);
	if(x == y)	return ;
	fa[x] = y;
}

int main() 
{
	int n,m;scanf("%d%d",&n,&m);
	forn(i,1,n)	scanf("%s",s[i] + 1);
	forn(i,1,n + m)	fa[i] = i;
	
	merge(1,1 + n),merge(1,m + n);
	merge(n,1 + n),merge(n,m + n);

	forn(i,1,n)	forn(j,1,m)
	{
		if(s[i][j] == '.')	continue;
		merge(i,j + n);
	}	
	
	set<int> st;
	int res = 1e9,tmp = 0;
	forn(i,1,n)	st.insert(find(i));
	res = min(res,(int)st.size());
	st.clear();
	forn(i,1,m)	st.insert(find(i + n));
	res = min(res,(int)st.size());
	printf("%d",res - 1);
	return 0;
}
posted @ 2021-02-14 17:05  随处可见的阿宅  阅读(291)  评论(0编辑  收藏  举报