AtCoder Grand Contest 009
A - Multiple Array
题解
可以发现只有给 (1sim n) 这个前缀整体加一个数才能让 (a_n) 变成 (b_n) 的倍数。所以直接从后往前推就行了。
实现
#include <cstdio>
using namespace std;
const long long NN = 1e5 + 5;
long long N, A[NN], B[NN];
long long ad[NN];
int main() {
freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
scanf("%lld", &N);
for (long long i = 1; i <= N; ++i) {
scanf("%lld%lld", &A[i], &B[i]);
long long tmp = A[i] / B[i];
if (A[i] % B[i] != 0) //注意特判
ad[i] = (tmp + 1) * B[i] - A[i];
}
for (long long i = N - 1; i >= 1; --i) {
if (ad[i] < ad[i + 1])
ad[i] += (ad[i + 1] - ad[i] + B[i] - 1) / B[i] * B[i];
}
printf("%lld
", ad[1]);
fclose(stdin);
fclose(stdout);
return 0;
}
总结
这种前缀的结构一把来说就是从后向前推,因为最后一个数只能被对前缀 (1sim n) 的操作改变。
B - Tournament
题解
建一棵树,将第 (i) 个人击败的人当成 (i) 结点的儿子。题就相当于给每条边标一个编号,一条边代表一场比赛,编号表示在比赛的那个二叉树(题面上画的那棵树)上的深度。那么一个点 (u) 到儿子 (v) 的边的编号必然要比 (u) 到 (u) 的父亲的边的编号大,而且任意两个 (u) 到儿子的边的编号不能相同。
所以考虑设 (f_i) 表示 (i) 的子树内的最大编号比 (i) 到父亲的编号至少大多少,才能满足以上的分配条件。在处理出 (u) 节点的 (f) 值之前,需要将 (u) 的儿子的 (f) 值预先处理好。然后将 (u) 的儿子按照 (f) 值从大到小排序,然后给 (u) 的每个到儿子的边赋编号,将 (f) 值大的编号赋小一点,这样可以让 (u) 子树内编号最大的尽量小。如果排序后为 (s_1,s_2,cdots s_m) ,那么 (f_u=max_{i=1}^{m}(f_{s_i}+i)) 。这样一层一层向上递推就行了。
实现
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int NN = 1e5 + 5;
int N, fa[NN], ans, trsz[NN], need[NN];
vector<int> tr[NN];
bool cmp(int i, int j) { return need[i] > need[j]; }
void Dfs(int u) {
for (int i = 0; i < trsz[u]; ++i)
Dfs(tr[u][i]);
sort(tr[u].begin(), tr[u].end(), cmp); //忘记排序了
for (int i = 0; i < trsz[u]; ++i)
need[u] = max(need[u], need[tr[u][i]] + i + 1);
}
int main() {
freopen("b.in", "r", stdin);
freopen("b.out", "w", stdout);
scanf("%d", &N);
for (int i = 2; i <= N; ++i) {
scanf("%d", &fa[i]);
tr[fa[i]].push_back(i);
}
for (int i = 1; i <= N; ++i)
trsz[i] = tr[i].size();
Dfs(1);
printf("%d
", need[1]);
fclose(stdin);
fclose(stdout);
return 0;
}
//不是深度+度数的最大值
//不是按照子树最大深度排序
总结
- 诸如按照子树的最大深度排序之类的贪心是错的。。
- 大胆猜测,小心验证(开始乱猜答案为所有点深度+度数的最大值)
这种递推还是很优美的。
C - Division into Two
题解
设 (f_i) 表示最后一个数分在 (A) 集合中的方案数。
那么 (f_i) 可以从 (f_j(j<i)) 转移当且仅当 (a_i-a_jge A) ,并且 (a_{j+1sim i}) 这些数可以都放到 (B) 中。但是是不是有可能 (a_{j+1}) 与 (a_{j-1}) 不能放在一块呢?如果不能放在一块,那么 (a_{j+1},a_{j-1},a_j) 两两不能放在一块,显然无解。所以先特判一些是否有解,然后直接 dp 就无论如何都能转移了。
容易发现可以转移的是一段区间,前缀和优化转移即可。并不需要什么树状数组/线段树之类的乱七八糟数据结构。
总结
总是容易陷入误区诶。。。陷入诸如“设 (f_{i,j}) 表示 (A) 中最后一个数是 (a_i) ,(B) 中最后一个数是 (b_i) 的方案数” 的误区很常见欸。这样好像听说可以线段树优化?不过我不是很懂。
实现
#include <cstdio>
#include <iostream>
using namespace std;
const int NN = 1e5 + 5, Mod = 1e9 + 7;
int Ad(int x, int y) { return ((x + y) > Mod) ? (x + y - Mod) : (x + y); }
int Dc(int x, int y) { return ((x - y) < 0) ? (x - y + Mod) : (x - y); }
int N, f[NN], lft[NN], s[NN];
long long A, B, a[NN];
bool can[NN];
int sum(int L, int R) {
if (L < 0 || R < 0) return 0;
if (L > R) return 0;
return Dc(s[R], s[L - 1]);
}
int main() {
scanf("%d%lld%lld", &N, &A, &B);
if (A < B) swap(A, B); //这道题的精髓所在
for (int i = 1; i <= N; ++i)
scanf("%lld", &a[i]);
for (int i = 2; i <= N - 1; ++i) {
if (a[i + 1] - a[i - 1] < B) {
printf("0
");
return 0;
}
}
for (int i = 2; i <= N; ++i) {
can[i] = ((a[i] - a[i - 1]) >= B);
if (can[i])
lft[i] = lft[i - 1];
else lft[i] = i;
}
int pt = 1;
s[0] = f[0] = 1;
for (int i = 1; i <= N; ++i) {
while (pt < i && a[i] - a[pt] >= A) ++pt;
//lft[i - 1] - 1 <= j <= pt - 1
f[i] = sum(max(lft[i - 1] - 1, 0), pt - 1);
s[i] = Ad(s[i - 1], f[i]);
// printf("%lld i : %d L : %d R : %d f[i] : %d
", a[i], i, max(lft[i - 1] - 1, 0), pt - 1, f[i]);
}
int ans = 0;
for (int i = N; i >= 0; --i) { //这里是 >=0
ans = Ad(ans, f[i]);
if (!can[i + 1] && i != N)
break ;
}
printf("%d
", ans);
return 0;
}
D - Uninity
题解
事实上题目等价于找出一种最优的指定分治中心的方法,使得点分树深度最小。
设每个点在点分树上的深度为 (d_i) ,设 (a_i=M-d_i) (其中 (M) 为所有 (d_i) 的最大值)。那么这样的 (a_{1sim n}) 可以唯一对应一棵点分树。那么什么样的 (a_{1sim n}) 能对应出一棵点分树呢?容易发现树上任何两个点 (x) 和 (y) 满足 (a_x=a_y) ,在 (x) 到 (y) 的路径上必存在一个点满足 (a_z>a_x) 并且 (a_z>a_y) 。只要满足这一条件,那么每回必能选出每一连通块内的 (a_i) 最大的结点(重要的是,这个点必然唯一,因为任意两个相同权值的点路径上都会有比这个点权值大的点)并将它作为分治重心,然后可以将这个点去掉形成若干连通块并继续递归地进行下去。如果构建出满足这个条件(指大小关系这个条件)的数组 (b) 并微调就可以得到 (a) 数组。题目让你做的实际上就是让 (b) 数组的最大值最小。
考虑将树以 1 为根节点,然后考虑给一个点标号。考虑这个点 (u) 不能标什么。对于 (u) 的子树内结点 (v) ,若 (v) 的 (b) 值为到 (u) 的路径上的最大值,那么 (u) 就不能填 (v) 的值。如果 (u) 有两棵子树内有相同的 (b) 值,那么 (u) 必须填入比这个值大的数。在满足这些事情的条件下,贪心地考虑将 (u) 所填的编号尽量小即可。由于树的高度最多为 (log _2 n) 级别的,所以直接用一个 ( ext{bitmask}) 存储一个点不能填哪些数即可。复杂度 (mathcal O(nlog _2n)) 不过好像可以 (mathcal O(n)) ?我也不知道。
总结
想到要研究所有点的点分树上的深度,并找到什么样的 (a) 数组符合条件是很重要的一步。
实现
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int NN = 1e5 + 5;
int N, head[NN], totE;
int mask[NN], ans;
struct E {
int v, nxt;
E(int _v, int _nxt) : v(_v), nxt(_nxt) {}
E() {}
} edge[NN * 2];
void AddE(int u, int v) {
edge[++totE] = E(v, head[u]);
head[u] = totE;
}
int Get(int num, int wei) { return (num >> wei) & 1; }
void Dfs(int u, int f) {
int cnt[20];
memset(cnt, 0, sizeof(cnt));
for (int p = head[u]; p; p = edge[p].nxt) {
int v = edge[p].v;
if (v != f) {
Dfs(v, u);
for (int i = 0; i < 17; ++i)
cnt[i] += Get(mask[v], i);
mask[u] |= mask[v];
}
}
int least = 0;
for (int i = 0; i < 17; ++i)
if (cnt[i] >= 2)
least = max(least, i + 1);
while (Get(mask[u], least)) ++least;
mask[u] >>= least;
mask[u] <<= least;
mask[u] |= (1 << least);
ans = max(ans, least);
}
int main() {
freopen("d.in", "r", stdin);
freopen("d.out", "w", stdout);
scanf("%d", &N);
for (int i = 1; i < N; ++i) {
int u, v;
scanf("%d%d", &u, &v);
AddE(u, v);
AddE(v, u);
}
Dfs(1, 0);
printf("%d
", ans);
fclose(stdin);
fclose(stdout);
return 0;
}
E - Eternal Average
题解
问题实际上相当于一棵 (k) 叉树,所有叶子结点都有一个 0 或 1 的数,每个非叶子结点上的数为叶子节点上的数的平均值。
考虑根节点的值得表达式。其实就是 (displaystyle sum_{iin leaf}frac{val_i}{K^{d_i}}) ,其中 (leaf) 表示叶子集合,(val_i) 表示结点 (i) 上填的数。
那么如果根节点的值为 (z) ,设 (z) 表示为 (K) 进制小数为 (0.c_1c_2cdots c_{len}) ,那么 (displaystyle sum_{i=1}^{len} c_iequiv Mpmod {K-1}) (感觉这里官方题解搞错了?(N,M) 可能他搞反了)。并且还需要 (displaystyle sum_{i=1}^{len} c_ile M) ,(1-z) 的数位和 (le N) ,(1-z) 的数位和为 (displaystyle (sum_{i=1}^{len-1}K-1-c_i)+K-c_{len}=Kcdot len-(len-1)-sum_{i=1}^{len}c_i=lencdot(K-1)+1-sum_{i=1}^{len}c_i) ,并且模 (K-1) 与 (N) 同余。似乎还需要枚举一共有多少位(因为最后有多少位是 0 是不确定的,虽然总共的位数有限)。
那么剩下的问题就是一个比较直接的 dp 可以解决的了。设 (displaystyle f_{i,j,0/1}) 表示前 (i) 位,总共用了 (j) 个数,最后一位是否为 0. 注意,小数最多可以有 (displaystyle frac{N+M-1}{K-1}) 位。转移是 (f_{i,j,0}=f_{i-1,j,0}+f_{i-1,j,1}) ,(displaystyle f_{i,j,1}=left(sum_{k=1}^{min(K-1,j)}f_{i-1,j-k,0}+f_{i-1,j-k,1} ight)) .
总结
小数的分母都是 (k) 的次幂,所以要化为 (k) 进制。其他的静下心想还能想出来,(k) 进制也勉强能够想出来吧。关键是静下心想!
实现
#include <cstdio>
#include <iostream>
using namespace std;
const int NN = 2e3 + 5, NM = 2e3 + 5, Mod = 1e9 + 7;
inline int Ad(int x, int y) { return ((x + y) > Mod) ? (x + y - Mod) : (x + y); }
inline int Dc(int x, int y) { return ((x - y) < 0) ? (x - y + Mod) : (x - y); }
int N, M, K;
int f[NN + NM][NM][2], s[NM];
int Sum(int L, int R) {
if (L < 0 || R < 0 || R < L)
return 0;
return Dc(s[R], s[L - 1]);
}
int main() {
freopen("e.in", "r", stdin);
freopen("e.out", "w", stdout);
scanf("%d%d%d", &N, &M, &K);
f[0][0][0] = 1;
int len = (N + M - 1) / (K - 1);
int ans = 0;
for (int i = 1; i <= len; ++i) {
s[0] = Ad(f[i - 1][0][0], f[i - 1][0][1]);
for (int j = 1; j <= M; ++j)
s[j] = Ad(s[j - 1], Ad(f[i - 1][j][0], f[i - 1][j][1]));
for (int j = 0; j <= M; ++j) {
f[i][j][0] = Ad(f[i - 1][j][0], f[i - 1][j][1]);
f[i][j][1] = Sum(j - min(K - 1, j), j - 1);
}
for (int j = 0; j <= M; ++j) {
int tmp = i * (K - 1) + 1 - j;
if (j % (K - 1) == M % (K - 1) && tmp <= N && tmp % (K - 1) == N % (K - 1))
ans = Ad(ans, f[i][j][1]);
}
}
printf("%d
", ans);
fclose(stdin);
fclose(stdout);
return 0;
}