比赛地址
弱校连萌寒假专题二
A. Cube Stacking
题意:
有N个积木,编号1~N,初始情况下每个积木单独占一个位置,每个积木上面都没有其他积木。有P个操作,操作分为移动和统计,将含有X号积木的柱子移动到含有Y号积木的柱子上,或统计在X号积木上面的积木个数。
N <= 30000, P <= 100000。
题解:
考虑带权并查集,由于每次只会增加柱子的高度,所以我们定义每个柱子的最底层积木为根节点,上面的积木是下面的积木的儿子,他们之间有一条权值为1的边,则在某个积木下的积木个数可由到根节点的距离dist表示。为了支持查询操作,我们还需要记录每个树的大小size。
加上路径压缩后时间复杂度O(nα(n))。
代码:
#include <cstdio>
const int maxn = 30001;
int p, x, y, fa[maxn], size[maxn], dist[maxn];
char op[2];
int find(int x)
{
if(x == fa[x])
return x;
int t = fa[x];
fa[x] = find(fa[x]);
dist[x] += dist[t];
return fa[x];
}
int main()
{
while(scanf("%d", &p) == 1)
{
for(int i = 1; i < maxn; ++i)
{
fa[i] = i;
size[i] = 1;
dist[i] = 0;
}
while(p--)
{
scanf("%s", op);
if(op[0] == 'M')
{
scanf("%d%d", &x, &y);
x = find(x), y = find(y);
if(x != y)
{
fa[y] = x;
dist[y] = size[x];
size[x] += size[y];
}
}
else
{
scanf("%d", &x);
printf("%d\n", size[find(x)] - dist[x] - 1);
}
}
}
return 0;
}
B. 修路
题意:
现在有n个点,m条边,问最少添加多少条边使得这n个点连通。
n <= 10^5, m <= 10^6。
题解:
利用并查集算出连通分量的个数,每添加一条边可以将两个连通分量合并成一个,所以答案是分量的个数减1。时间复杂度O(nα(n))。
代码:
#include <cstdio>
const int maxn = 1e5 + 1;
int n, m, fa[maxn], u, v, ans;
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
int main()
{
while(scanf("%d%d", &n, &m) == 2)
{
for(int i = 1; i <= n; ++i)
fa[i] = i;
while(m--)
{
scanf("%d%d", &u, &v);
fa[find(u)] = find(v);
}
ans = -1;
for(int i = 1; i <= n; ++i)
if(i == find(i))
++ans;
printf("%d\n", ans);
}
return 0;
}
C. Frogger
题意:
二维平面上有n个落脚点,每次可以从一个落脚点跳跃到另一个落脚点,距离为欧式距离,问从第一个落脚点跳跃到第二个落脚点的过程中最大的跳跃距离可能的最小值。
2 <= n <= 200, |坐标| <= 1000。
题解:
直接枚举跳跃过程的方案肯定是不行的,不妨考虑枚举一个答案的限界,在距离小于限制的点对互相连通,利用并查集判断起点与终点是否连通即可。
发现到随着限界的递增,答案从无效变为有效,可以利用二分计算出有效的答案的最小值。时间复杂度O(nα(n)logK),其中K是坐标的范围。
代码:
#include <cmath>
#include <cstdio>
const int maxn = 201;
int n, x[maxn], y[maxn], fa[maxn], L, R, M;
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
bool check()
{
for(int i = 0; i < n; ++i)
fa[i] = i;
for(int i = 0; i < n; ++i)
for(int j = i + 1; j < n; ++j)
if((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]) <= M)
fa[find(j)] = find(i);
return find(0) == find(1);
}
int main()
{
for(int Case = 1; scanf("%d", &n) != EOF && n; ++Case)
{
for(int i = 0; i < n; ++i)
scanf("%d%d", x + i, y + i);
L = 0, R = 2e6;
while(L < R)
{
M = L + R >> 1;
if(check())
R = M;
else
L = M + 1;
}
if(Case > 1)
puts("");
printf("Scenario #%d\nFrog Distance = %.3f\n", Case, sqrt(L));
}
return 0;
}
D. Supermarket
题意:
有n个货物需要清理,但每天只能清理一个货物,每个货物有一个价值p和一个清理时间的截止日期d,求最多能清理获得的价值之和。
n, p, d <= 10000。
题解:
不难想到贪心的方法,按照价值降序处理每个货物,货物尽量推迟到当前可选的最晚的时间售出,计算能售出的货物价值之和。
当前可选的时间直接进行计算会使得总时间复杂度为O(nlogn+nd)。
考虑到出现枚举的过程重复跳跃了一段段不可选的日子,如果能快速计算出每一天及每一天之前最晚还没被占用的日子,问题就能得到解决。
我们发现路径压缩后的并查集正能做到这一点,定义被占用的日子的父亲是该日子左边的日子,则根节点一定是没被占用的日子且不晚于该树上的节点,每次占用某个日子再连边即可。
时间复杂度O(nlogn)。
代码:
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 10001, maxd = 10001;
int n, fa[maxd], ans;
struct Node
{
int p, d;
bool operator < (const Node &x) const
{
return p > x.p;
}
} a[maxn];
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
int main()
{
while(scanf("%d", &n) != EOF)
{
for(int i = 0; i < maxd; ++i)
fa[i] = i;
for(int i = 0; i < n; ++i)
scanf("%d%d", &a[i].p, &a[i].d);
sort(a, a + n);
ans = 0;
for(int i = 0; i < n; ++i)
{
int x = find(a[i].d);
if(x)
{
fa[x] = find(x - 1);
ans += a[i].p;
}
}
printf("%d\n", ans);
}
return 0;
}
E. 食物链
题意:
有n个动物,动物之间的食物链关系成三元环,具体的表现是:若A吃B,B吃C,则C吃A。
现在在不知道食物链具体形状的情况下检查一个人说的k句话是否与前面的话有矛盾或者是捏造的假话,统计不对的话的个数。
话只有两种,X和Y是同类,或者X吃Y。
n <= 50000, k <= 100000。
题解:
考虑带权并查集的话,如果X吃Y,则令X是Y的父亲,边权为1,按照定义,这样可以看出一颗树里距离为3的倍数的点对都是同类,因此还可以将到根节点的距离dist模3,用0、1、2代表同类、天敌和食物,对于每一句话检查是否有矛盾即可。
还有一种不带权并查集的方法,对每个动物i先虚构出一个天敌n+i和一个食物2n+i,动物自己就是自己的同类,这样只需要3n个节点来表示每个动物的同类、天敌和食物,然后按照关系来检查真假、合并关系即可。时间复杂度为O(kα(n))。
代码:(第二种解法)
#include <cstdio>
const int maxn = 50001;
int n, k, fa[maxn * 3], ans;
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
int main()
{
scanf("%d%d", &n, &k);
ans = 0;
for(int i = 1; i <= n * 3; ++i)
fa[i] = i;
while(k--)
{
int d, x, y;
scanf("%d%d%d", &d, &x, &y);
if(x > n || y > n)
{
++ans;
continue;
}//同类 被吃 吃
int fx1 = find(x), fx2 = find(x + n), fx3 = find(x + n + n);
int fy1 = find(y), fy2 = find(y + n), fy3 = find(y + n + n);
if(d == 1)
{
if(fx1 == fy2 || fx1 == fy3 || fy1 == fx2 || fy1 == fx3)
{
++ans;
continue;
}
fa[find(fx1)] = find(fy1);
fa[find(fx2)] = find(fy2);
fa[find(fx3)] = find(fy3);
}
else
{
if(fx1 == fy1 || fx1 == fy3 || fx2 == fy1)
{
++ans;
continue;
}
fa[find(fx1)] = find(fy2);
fa[find(fx2)] = find(fy3);
fa[find(fx3)] = find(fy1);
}
}
printf("%d\n", ans);
return 0;
}
F. Cutting Cake
题意:
初始有一块N * N格的蛋糕,现在要对蛋糕进行M次切割,每次会从蛋糕中选择一块矩形的区域,由于之前的切割,区域内可能不是一整块蛋糕,你需要对于每次切割都计算出区域内有多少块互不接触的蛋糕。
n <= 1000, m <= 10000。
题解:
考虑对于每个操作直接枚举对应区域还残留的蛋糕格,这可以利用搜索来快速计算四连通块数并删除蛋糕格,但时间复杂度较大。
原因还是出现了过多的对一块块空蛋糕格的重复枚举,可以考虑用并查集的性质快速跳跃到需要的节点,由于每个节点只会被访问到一次且访问结束后立即删除,所以时间复杂度得到了保证。
二维的跳跃很难实现,但是一维的还是很简单,每一行维护一下即可,每个询问枚举每一行,找到一个点即开始搜索。时间复杂度O(n^2α(n)+nm)。
代码:(与解法的内容略有出入)
#include <cstdio>
const int maxn = 1010, maxm = 10001;
int n, m, pos[maxn][maxn], fa[maxn][maxn], que[maxn * maxn], l, r, ans[maxm];
int find(int x, int y)
{
return y == fa[x][y] ? y : fa[x][y] = find(x, fa[x][y]);
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 0; i < n; ++i)
for(int j = 0; j <= n; ++j)
fa[i][j] = j;
for(int t = 1, x1, y1, x2, y2; t <= m; ++t)
{
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
--x1, --y1, --x2, --y2;
for(int i = x1; i <= x2; ++i)
for(int j = find(i, y1); j <= y2; j = find(i, j))
{
pos[i][j] = t;
fa[i][j] = j + 1;
}
}
for(int i = 0; i < n; ++i)
for(int j = 0; j < n; ++j)
if(pos[i][j])
{
int idx = pos[i][j];
++ans[idx];
l = r = 0;
que[r++] = i * n + j;
pos[i][j] = 0;
while(l < r)
{
int x = que[l] / n, y = que[l++] % n;
if(x > 0 && pos[x - 1][y] == idx)
{
que[r++] = (x - 1) * n + y;
pos[x - 1][y] = 0;
}
if(x < n - 1 && pos[x + 1][y] == idx)
{
que[r++] = (x + 1) * n + y;
pos[x + 1][y] = 0;
}
if(y > 0 && pos[x][y - 1] == idx)
{
que[r++] = x * n + y - 1;
pos[x][y - 1] = 0;
}
if(y < n - 1 && pos[x][y + 1] == idx)
{
que[r++] = x * n + y + 1;
pos[x][y + 1] = 0;
}
}
}
for(int i = 1; i <= m; ++i)
printf("%d\n", ans[i]);
return 0;
}
G. Rochambeau
题意:
有n个人,编号0~n-1,其中有一个人是裁判,其他的每个人只会出石头剪刀布中的一种,裁判和其他人猜拳无论出什么都能胜过其他人,其他人之间猜拳按照正常的规则出结果,给出m场猜拳的结果,问是否能判断哪个人一定是裁判,以及在第几步一定能判断出,否则判断是无解还是无法判断裁判是谁。
n <= 500, m <= 2000。
题解:
石头剪刀布的规则暗示他们是一个三元环,可以考虑带权并查集或者三倍并查集来判断是否出现矛盾。
由于裁判的特殊性,我们只好枚举裁判是谁,判断其他人的猜拳结果是否冲突。
如果有两个及以上的人可以是裁判则无法判断,如果没有人可以是裁判则无解。
现在考虑如何在知道第几步即可判断出是裁判,这可以通过记录其他人被假想为裁判时出现矛盾的场次,其中的最大值即为答案,因为到达这一步已经判断出了其他人不可能是裁判。
时间复杂度O(nmα(n))。
代码:
#include <cstdio>
const int maxn = 501, maxm = 2001;
int n, m, fa[maxn * 3], query[maxm][3], id, pos, cnt;
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
int main()
{
while(scanf("%d%d", &n, &m) == 2)
{
id = pos = cnt = 0;
for(int i = 1; i <= m; ++i)
{
int x, y;
char op;
scanf("%d%c%d", &x, &op, &y);
if(op == '=')
{
query[i][0] = 1;
query[i][1] = x + 1;
query[i][2] = y + 1;
}
else if(op == '<')
{
query[i][0] = 2;
query[i][1] = y + 1;
query[i][2] = x + 1;
}
else
{
query[i][0] = 2;
query[i][1] = x + 1;
query[i][2] = y + 1;
}
}
for(int i = 1; i <= n; ++i)
{
for(int j = 1; j <= n * 3; ++j)
fa[j] = j;
bool flag = 0;
for(int j = 1; j <= m; ++j)
{
int &d = query[j][0], &x = query[j][1], &y = query[j][2];
if(x == i || y == i)
continue;
int fx1 = find(x), fx2 = find(x + n), fx3 = find(x + n + n);
int fy1 = find(y), fy2 = find(y + n), fy3 = find(y + n + n);
if(d == 1)
{
if(fx1 == fy2 || fx1 == fy3 || fy1 == fx2 || fy1 == fx3)
{
flag = 1;
if(pos < j)
pos = j;
break;
}
fa[find(fx1)] = find(fy1);
fa[find(fx2)] = find(fy2);
fa[find(fx3)] = find(fy3);
}
else
{
if(fx1 == fy1 || fx1 == fy3 || fx2 == fy1)
{
flag = 1;
if(pos < j)
pos = j;
break;
}
fa[find(fx1)] = find(fy2);
fa[find(fx2)] = find(fy3);
fa[find(fx3)] = find(fy1);
}
}
if(!flag)
{
id = i;
++cnt;
if(cnt > 1)
break;
}
}
if(!cnt)
puts("Impossible");
else if(cnt == 1)
printf("Player %d can be determined to be the judge after %d lines\n", id - 1, pos);
else
puts("Can not determine");
}
return 0;
}
H. Code Lock
题意:
有一个长度为n的密码锁,锁的每一位都是小写字母,现在有m种操作,每次可以将一段区间的密码循环到下一个字母,如果一串密码经过一些操作之后可以得到另一串密码,则认为这两串密码是同构的,求有多少不同构的密码,答案模10 ^ 9 + 7。
n <= 10 ^ 7, m <= 1000。
题解:
如果任意区间或拼凑起来的区间都不能被其他区间拼凑得到,那么这些区间是互相独立的,每一个都会使得密码锁的不同构方案除去字母个数26,没有任何区间的时候答案为26 ^ n,有k个区间的时候答案即为26 ^ (n - k)。
考虑如何统计独立的区间,如果有[l, r], [l, x], [x + 1, r]三个区间,则只有任意两个是有效的,可以考虑将每个区间转化为两个前缀区间来考虑,即[1, r] - [1, l - 1], [1, x] - [1, l - 1], [1, r] - [1, x],则可以将前缀区间利用并查集合并,维护连通性从而计算出独立区间的个数,很明显操作的顺序不会改变答案的大小。
时间复杂度O(mα(n) + logn)。
代码:
#include <cstdio>
const int mod = 1000000007, maxn = 10000010;
int n, m, fa[maxn], tot;
int pow(int x, int k)
{
int ret = 1;
while(k)
{
if(k & 1)
ret = (long long)ret * x % mod;
x = (long long)x * x % mod;
k >>= 1;
}
return ret;
}
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
int main()
{
while(scanf("%d%d", &n, &m) == 2)
{
tot = 0;
for(int i = 1; i <= n + 1; ++i)
fa[i] = i;
while(m--)
{
int l, r;
scanf("%d%d", &l, &r);
if((l = find(l)) != (r = find(r + 1)))
{
fa[l] = r;
++tot;
}
}
printf("%d\n", pow(26, n - tot));
}
return 0;
}