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;
}