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)则枚举(Bin[L,R]),所以(B+Cin[L + B,R + B]),那么事实上能拼凑出合理的(Ain[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
",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;
}
原文地址:https://www.cnblogs.com/HotPants/p/14402231.html