本场链接: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;
}