T1:Count
题面:
【问题描述】
李华终于逃离了无尽的英语作文, 重获自由的他对一棵树产生了兴趣。 首先,他想知道一棵树是否能分成大小相同的几块(即切掉一些边,使得每个连通块的点数相同)。然后,他觉得这个问题过于简单,于是他想知道一共有多少种方案可以把这棵树分成大小相同的几块。
然后他发现自己不会了,于是向聪明的你求助。
【输入格式】
第一行一个数,表示数的大小。 第二行至第N行,每行两个数x,y表示x和y之间有一条边。
【输出格式】
一行,表示方案数。
【样例一输入】
6
1 2
2 3
2 4
4 5
5 6
【样例一输入】
3
HINT:
对于30%的数据,1<=n<=100。 对于60%的数据,1<=n<=100000。
对于100%的数据,1<=n<=1000000。
解析:
比较基础的一道题吧,显然枚举块的大小,块的大小一定为树的大小的约数, 枚举从$1$到树的$size$之间的数, 如果是约数就去树上$check$, 设该树为$s$, 只有某子树的大小恰好为$s$时, 会形成一个大小为$s$的块, 将该子树的$size$改为0, 原树就被割小了, 再进行相同操作即可。我们发现割树的方式是唯一的(大小为$s$子树的分割方式只有一种),因此如果一棵树可以被分割成多个大小为$s$的块,它对答案的贡献只有1
代码:
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; const int maxn = 1000004; inline int read() { int ret, f = 1; char c; while((c=getchar())&&(c<'0'||c>'9'))if(c=='-')f=-1; ret=c-'0'; while((c=getchar())&&(c>='0'&&c<='9'))ret = (ret<<3)+(ret<<1)+c-'0'; return ret*f; } int n, ans = 1; int head[maxn], tot; struct edge{ int nxt, to; }e[maxn<<1]; void Addedge(int x, int y) { e[++tot] = (edge){head[x], y}; head[x] = tot; } int siz[maxn], nowsiz[maxn]; void dfs(int x, int fa) { siz[x] = 1; for(int i = head[x]; i; i = e[i].nxt) { int id = e[i].to; if(id == fa) continue; dfs(id, x); siz[x] += siz[id]; } } int blo; void dfs2(int x, int s, int fa) { if(siz[x] < s) { nowsiz[x] = siz[x]; return ; } nowsiz[x] = 1; for(int i = head[x] ; i; i = e[i].nxt) { int id = e[i].to; if(id == fa) continue; dfs2(id, s, x); nowsiz[x] += nowsiz[id]; if(nowsiz[id] > s) return ; } if(nowsiz[x] == s) { nowsiz[x] = 0; blo ++; } } int check(int x) { blo = 0; dfs2(1, x, 0); return (blo == n/x); } int main() { freopen("count.in", "r", stdin); freopen("count.out", "w", stdout); n = read(); for(int i = 1; i < n; ++i) { int u = read(), v = read(); Addedge(u, v); Addedge(v, u); } dfs(1, 0); for(int i = 2; i <= n; ++i) if(n % i == 0) ans += check(i); printf("%d\n", ans); return 0; }
T2:Dinner
题面:
【问题描述】
清儿今天请好朋友们吃饭,一共 N 个人坐在坐在圆桌旁。 吃饭的第一步当然是点餐了。服务员拿来了 M 份菜单。第 i 个人阅读菜单并点出自己喜欢的菜需要花费时间 T[i]。 当一个人点完菜之后,就会把菜单传到他右手边的第一个人。 M 份菜单是同时发出的,每个菜单只能同时被一个人阅读。 清儿希望知道如何分发菜单,才能让点餐的总时间花费最少呢?
【输入格式】
输入文件名为 dinner.in
输入第一行是 N 和 M,表示人数和菜单数 输入第二行,N 个数,表示每个人点餐所需要的时间。
【输出格式】
输出文件名为 dinner.out
输出一个整数表示点餐花费的最小时间。
【样例一输入】
3 2
1 5 10
【样例一输出】
10
【样例二输入】
4 2
1 2 3 4
【样例二输出】
5
HINT: 对于 20%的数据,n<=100,m<=100. 对于 60%的数据,n<=10000,m<=100.
对于 100%的数据,n<=50000,T[i]<=600,m<=3000.
解析:
首先是基础操作, 把环断开, 再复制一遍序列在原序列后, 设该数列为$T[i]$
要求最长的时间最短, 显然可以二分答案,如何$check$, 假设我们要$check$的数是$mid$
很容易想到一个暴力的$check$, 枚举环的断点,向后跳,把环分成多个区间,每个区间的和 $ <= mid$, 最后把区间个数与$m$比较, 时间复杂度为$O(N^{2}logK)$(其中$K$为值域范围)
然后也容易想到一个小优化, 根据常识, $T[i]$为正, 在枚举断点向后跳时,可以利用前缀和与二分在$logN$的时间内找到右端点,此时时间复杂度为$O(NMlogNlogK)$
考场上我想到这里就跑路了,因为一开始读题时感觉第三题更可做,结果发现题读错了, 亏得不谈, 也算是个教训吧, 考完后我接着想了一下,想出了正确的解法(不是倍增)
考虑我们的算法慢在哪里。我设$s[i]$为数组的前缀和, 若$s[r] <= mid$ && $s[r+1] > mid$, 对于一种序列的分割,在分割完成后就被分成了多个区间, 而必定有一个区间的左端点出现在$[1, r + 1]$内, 因此只需要枚举$[1, r + 1]$即可。为什么$[r + 2, n]$就不用枚举了呢, 因为在其中任意一个点开始分割,一定会跳到$[1, r + 1]$中, 设跳到的点为$p$, 所分割出来的块的个数与从点$p$开始分割的个数是一样的, 这就完成了时间复杂度的优化。
为什么要区间是$[1, r + 1]$而不是$[1, r]$呢?因为原问题是一个环,虽然$s[r] <= mid$ && $s[r+1] > mid$, 但也有可能$T[n] + s[r] <= mid$ && $s[r+1] > mid$, 这样的式子还可以写下去,简而言之就是$[1, r]$的左端点可以向左延伸(原问题是环),但右端点不行,无论我从点$1$左边的哪个点开始跳, 有可能跳过$[1, r]$, 但也一定会在$r + 1$点停下,这样正确性就可以保证了
最终的时间复杂度是$O(SizeMlogNlogK)$(其中$Size$是第一个块的大小, 即$r + 1$), 实际上远远达不到这个上界, 有些时候不用跳$m$次就跳完序列了, 这种时候$Size$都不用枚举完, 反正随机数据是随便过的, 刻意构造的话就不得而知了
代码:
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; const int maxn = 50004; inline int read() { int ret, f = 1; char c; while((c=getchar())&&(c<'0'||c>'9'))if(c=='-')f=-1; ret=c-'0'; while((c=getchar())&&(c>='0'&&c<='9'))ret = (ret<<3)+(ret<<1)+c-'0'; return ret*f; } int n, m, mx, ans, sum, a[maxn<<1], s[maxn<<1]; int search(int x, int l, int r) { int t = l - 1, mid, ret = l; while(l <= r) { mid = (l + r)>>1; if(s[mid] - s[t] <= x) ret = mid, l = mid + 1; else r = mid - 1; } return ret; } bool check(int x) { int q = search(x, 1, n); for(int i = 1; i <= q + 1; ++i) { int k = 0, j = i; while(j < n + i) { int p = search(x, j, n + i - 1); k ++; j = p + 1; if(k > m) break; } if(k <= m) return 1; } return 0; } int main() { freopen("dinner.in", "r", stdin); freopen("dinner.out", "w", stdout); n = read();m = read(); for(int i = 1; i <= n; ++i) { a[i] = a[n+i] = read(); mx = max(mx, a[i]); sum += a[i]; } for(int i = 1; i <= (n<<1); ++i) s[i] = s[i-1] + a[i]; int l = mx, r = sum, mid; while(l <= r) { mid = (l + r)>>1; if(check(mid)) ans = mid, r = mid - 1; else l = mid + 1; } printf("%d\n", ans); return 0; }
T3:Chess
题面:
【问题描述】 pig 在下象棋的时候特别喜欢用马,他总是计算着自己的马还需要几步才能吃掉对方的帅,以及方案数的个数,当然 pig 很笨,所以他只能求助于你。 我们假设在 m*n 的棋盘上,pig 的马只能走 1*2 的格点,他的马一开始在 st,对方的帅在 ed。当然,我们不能忽视友军和敌军,所以如果落点被友军占有,那么就不能飞过去了;如果落点被敌军占有,那么 pig 认为自己这一步赚了,所以不计入总步数。为了简化问题,我们的马在飞的时候不受到敌军限制
【输入格式】
第一行两个数 m,n 表示棋盘大小为 m*n。 接下来 m 行,每行 n 个数字, 0 表示格子为空,1 表示格子上有敌军,2 表示格子上有友军,3 表示马的位置,4 表示敌方的帅。
【输出格式】
两行。 第一行:一个数,最少情况下实际走的步数。如果没有方案存在,输出- 1。 第二行:一个数,达到最小值的方案总数,如果两个方案走的空格不同则认为这两个方案不同(详见样例)。这个数保证不超过内设64位整数(long long/ int64)的大小。如果第一行是-1,不要输出此行。
【样例一输入】
4 5
0 0 0 0 1
0 0 3 0 0
0 0 0 4 0
0 1 0 0 0
【样例一输出】
0
1
【样例二输入】
4 5
1 0 0 0 0
3 0 0 0 0
0 0 2 0 0
0 0 0 4 0
【样例二输出】
2
3
【样例二解释】
一共有3种方案。X代表实际走的步数
10000 10X00 10X00
30X00 30000 3000X
00200 0X200 00200
0X040 00040 00040
HINT:落在敌军的位置敌军就会被吃掉。不要想在两个位置来回跳跃。
对于30%的数据,m,n<=5。 对于100%的数据,m,n<=50。
解析:
一开始看错题了,结果只有30分
还是直接说正解吧。其实就是最短路计数问题,但是需要一些转化。如果两个$0$点能直接相连, 就连一条权值为$1$的边。如果遇到没有访问过的$1$点,就去$dfs$一次, 把走一步能到的$0$点(包括先走几个$1$点才能到的$0$点)存入一个栈内,这些栈内的不同的点间也建一条权值为$1$的边, 把这些访问过的$1$点打上标记。最后在新图上跑最短路计数即可。
这里的边权都为1,因此$bfs$即可。注意由于新图的边的条数不好计算,就根据空限尽量开大一点吧。还有我不知道是否有必要,感觉是必要的吧,也没试过,反正我是判了重边的
代码:
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; typedef long long ll; const int maxn = 2510; int n, m, a[55][55], s[2], dis[maxn], ans, des; int stak[maxn], top; int q[maxn], h, t; bool vis[55][55], g[maxn][maxn]; ll way[maxn], num; int dx[8] = {1, 1, 2, 2, -1, -1, -2, -2}; int dy[8] = {-2, 2, -1, 1, 2, -2, 1, -1}; int head[maxn], tot; struct edge{ int nxt, to; }e[maxn<<5]; void Addedge(int x, int y) { e[++tot] = (edge){head[x], y}; head[x] = tot; } int bfs() { h = 1; q[++t] = (s[0] - 1) * n + s[1]; while(h <= t) { int x = q[h++]; if(x == des) { num = way[des]; return dis[des] - 1; } for(int i = head[x]; i; i = e[i].nxt) { int id = e[i].to; if(dis[id] != -1) { if(dis[id] == dis[x] + 1) way[id] += way[x]; } else { dis[id] = dis[x] + 1; way[id] = way[x]; q[++t] = id; } } } return -1; } void dfs(int x, int y) { vis[x][y] = 1; for(int i = 0; i < 8; ++i) { int tox = x + dx[i], toy = y + dy[i]; if(tox < 1 || tox > m) continue; if(toy < 1 || toy > n) continue; if(a[tox][toy] == 2) continue; if(a[tox][toy] != 1) stak[++top] = (tox - 1) * n + toy; else if(!vis[tox][toy]) dfs(tox, toy); } } int main() { freopen("chess.in", "r", stdin); freopen("chess.out", "w", stdout); scanf("%d%d", &m, &n); for(int i = 1; i <= m; ++i) for(int j = 1; j <= n; ++j) { char c; while((c=getchar())&&(c<'0'||c>'4')); a[i][j] = c - '0'; if(a[i][j] == 3) { s[0] = i; s[1] = j; way[(i - 1) * n + j] = 1; } else { dis[(i - 1) * n + j] = -1; if(a[i][j] == 4) des = (i - 1) * n + j; } } for(int i = 1; i <= m; ++i) for(int j = 1; j <= n; ++j) { if(a[i][j] == 2) continue; int now = (i - 1) * n + j; for(int k = 0; k < 8; ++k) { int tox = i + dx[k], toy = j + dy[k]; if(tox < 1 || tox > m) continue; if(toy < 1 || toy > n) continue; if(a[tox][toy] == 2) continue; if(a[i][j] != 1 && !g[tox][toy]) { g[now][(tox - 1) * n + toy] = 1; Addedge(now, (tox - 1) * n + toy); } } } for(int i = 1; i <= m; ++i) for(int j = 1; j <= n; ++j) if(a[i][j] == 1 && !vis[i][j]) { top = 0; dfs(i, j); for(int p = 1; p <= top; ++p) for(int k = 1; k <= top; ++k) if(stak[p] != stak[k] && !g[stak[p]][stak[k]]) { g[stak[p]][stak[k]] = 1; Addedge(stak[p], stak[k]); } } ans = bfs(); printf("%d\n", ans); if(ans != -1) printf("%lld", num); return 0; }