Codeforces Round #615 (Div. 3)
暂时没有F题的题解。太菜了。
A - Collecting Coins
题意:有三个人,分别有a,b,c枚硬币,你有n枚硬币,要求把n枚硬币全部给这三个人并且使得他们的硬币数变为相等。问是否可行。
题解:先验证硬币总数a+b+c+n是否能被3整除,然后验证要补的硬币的数量少于n。
void test_case() {
int a[5], n;
scanf("%d%d%d%d", &a[1], &a[2], &a[3], &n);
sort(a + 1, a + 1 + 3);
int sum = a[3] - a[1] + a[3] - a[2];
if(sum > n || (sum - n) % 3 != 0)
puts("NO");
else
puts("YES");
}
B - Collecting Packages
题意:有个机器人,只能向上走或者向右走,要从(0,0)经过所有的点(xi,yi),求是否可行,若可行则输出字典序最小的方案。
题解:字典序最小就是说先往右走再往上走。直接把所有点按x排序,要求每个点与(0,0)包围的矩形包含此前的所有点,只需要比前面的点都高就行了。由数学归纳法可知只需要比前一个点高就行了。
int n;
pii p[1005];
void test_case() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
p[i] = {x, y};
}
sort(p + 1, p + 1 + n);
for(int i = 2; i <= n; ++i) {
if(p[i].second < p[i - 1].second) {
puts("NO");
return;
}
}
puts("YES");
int cx = 0, cy = 0;
for(int i = 1; i <= n; ++i) {
while(cx < p[i].first) {
putchar('R');
++cx;
}
while(cy < p[i].second) {
putchar('U');
++cy;
}
}
putchar('\n');
}
C - Product of Three Numbers
题意:给一个数n,满足n>=2,要求将n分解成互异的三个数a,b,c,使得a*b*c==n,且a,b,c>=2。给出一种分解的方案,或者说明其不存在。
题解:一开始没看见>=2直接判断质数就上了(虽然这样也不对,比如9不能分解为1,3,3),仔细一想跟质数的种类有关系,所以可以直接分解质因数。假如有3种以上的质因数,那么前两个数要两种,剩下的最后一个数补位就是一种字典序最小的构造。假如只有1种质因数,那么肯定至少需要6次,前两个数分别要1次和2次,剩下的至少要3次,这也是一种字典序最小的构造。假如有2种质因数,那么总次数<=3的显然是无解的,而总次数>=4的必定有解。首先把两种质因数各1次拿出来,那么剩下的那个2次也不会和前面的重复,只是这样并不是字典序最小(还好题目没要求)。
最简单的思路也是字典序最小的思路就是直接暴力枚举a和b。
下面这个质因数分解的模板貌似还带有筛出最小质因子的功能的。
const int MAXN = 1e5;
int p[MAXN + 5], ptop;
int pn[MAXN + 5];
void sieve() {
int n = MAXN;
pn[1] = 1;
for(int i = 2; i <= n; i++) {
if(!pn[i])
p[++ptop] = i;
for(int j = 1; j <= ptop; j++) {
int t = i * p[j];
if(t > n)
break;
pn[t] = p[j];
if(i % p[j] == 0)
break;
}
}
}
int fac[105][2], ftop;
void get_fac(int n) {
ftop = 0;
for(int i = 1; i <= ptop; ++i) {
if(n % p[i] == 0) {
fac[++ftop][0] = p[i];
fac[ftop][1] = 0;
while(n % p[i] == 0) {
n /= p[i];
++fac[ftop][1];
}
}
}
if(n > 1) {
fac[++ftop][0] = n;
fac[ftop][1] = 1;
}
}
void test_case() {
int n;
scanf("%d", &n);
get_fac(n);
if(ftop >= 3) {
puts("YES");
printf("%d %d %d\n", fac[1][0], fac[2][0], n / fac[1][0] / fac[2][0]);
return;
} else if(ftop == 1) {
if(fac[1][1] >= 6) {
puts("YES");
printf("%d %d %d\n", fac[1][0], fac[1][0]*fac[1][0], n / fac[1][0] / fac[1][0] / fac[1][0]);
return;
} else {
puts("NO");
return;
}
} else {
int sum = fac[1][1] + fac[2][1];
if(sum <= 3) {
puts("NO");
return;
} else {
puts("YES");
printf("%d %d %d\n", fac[1][0], fac[2][0], n / fac[1][0] / fac[2][0]);
return;
}
}
}
D - MEX maximizing
题意:有q次操作和一个固定的正整数x,每次操作会往序列中加入一个数字y,你可以在任何时候对序列中的某个y进行任意多次加减x操作(但不能使他们变成负数),求使得整个序列的MEX最大的方案时的MEX的值。
题解:显然模x余数相同的都是同一种等价的东西,可以维护模x值不同结果的cnt。那么MEX操作就相当于询问整个cnt序列中的最小值(这个最小值是一层一层循环叠在下面把MEX垫高),然后再求序列中等于这个最小值的最左边的元素,这个显然可以用线段树来维护(甚至可以支持修改删除)。
但是最简单的操作还是在cnt里面数了之后不断尝试越过MEX即可。
不知道为什么这个操作恰好就是可以用pair的默认操作来维护,但是换成pair之后会编译失败。
struct SegmentTree {
#define ls (o<<1)
#define rs (o<<1|1)
static const int MAXN = 420000;
int st[(MAXN << 2) + 5];
int pos[(MAXN << 2) + 5];
void PushUp(int o) {
st[o] = min(st[ls], st[rs]);
if(st[rs] < st[ls])
pos[o] = pos[rs];
else
pos[o] = pos[ls];
}
void Build(int o, int l, int r) {
if(l == r) {
st[o] = 0;
pos[o] = l;
} else {
int m = l + r >> 1;
Build(ls, l, m);
Build(rs, m + 1, r);
PushUp(o);
}
}
void Update(int o, int l, int r, int p, int v) {
if(l == r) {
st[o] += v;
return;
} else {
int m = l + r >> 1;
if(p <= m)
Update(ls, l, m, p, v);
if(p >= m + 1)
Update(rs, m + 1, r, p, v);
PushUp(o);
}
}
pii QueryMin(int o, int l, int r, int ql, int qr) {
if(ql <= l && r <= qr) {
return {st[o], pos[o]};
} else {
int m = l + r >> 1;
int res = INF;
int resp = -1;
if(ql <= m) {
pii tmp = QueryMin(ls, l, m, ql, qr);
res = tmp.first;
resp = tmp.second;
}
if(qr >= m + 1) {
pii tmp = QueryMin(rs, m + 1, r, ql, qr);
if(tmp.first < res) {
res = tmp.first;
resp = tmp.second;
}
}
return {res, resp};
}
}
#undef ls
#undef rs
} st;
void test_case() {
int q, x;
scanf("%d%d", &q, &x);
st.Build(1, 1, x);
while(q--) {
int y;
scanf("%d", &y);
y = (y % x) + 1;
st.Update(1, 1, x, y, 1);
pii tmp = st.QueryMin(1, 1, x, 1, x);
printf("%d\n", tmp.first * x + (tmp.second - 1));
}
}
E - Obtain a Permutation
题意:给一个n*m的矩阵,要求复原成形如:
1 2 3 4
5 6 7 8
9 10 11 12
的样子,也就是依次填充。
有两种cost都是1的操作,1种是直接修改某位置的数字,另一种是把一列向上轮换。
首先,列与列之间显然是独立的,可以分开考虑,那么就考虑某列怎么复原。最简单的方法就是枚举这一列的开头在哪一行(也就是枚举需要向上),然后每个元素就知道是否对应当前是否处在正确的位置上,每个不在正确位置上的都必须要被换掉。这个复杂度是(单列)n^2的。观察一下容易发现每个元素最多在某次枚举中处在正确位置,这个关系要从等差数列公式推一推。
然后这样做只会会RE,因为访问到了负数。然后修复这个bug只会会WA,想一想其实还会上溢出,因为题目给的数字不一定在n*m以内,这样子确实还是会溢出。
vector<int> a[200005];
int pos[200005];
void test_case() {
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i) {
a[i].resize(m + 5);
for(int j = 1; j <= m; ++j)
scanf("%d", &a[i][j]);
}
ll sumcost = 0;
int C = n * m;
for(int j = 1; j <= m; ++j) {
int mincost = INF;
for(int i = 0; i < n; ++i)
pos[i] = 0;
for(int i = 1; i <= n; ++i) {
if(a[i][j] > C || (a[i][j] - j) % m != 0)
continue;
else {
int id = i - 1 - ((a[i][j] - j) / m);
if(id < 0)
id += n;
++pos[id];
}
}
for(int i = 0; i < n; ++i)
mincost = min(mincost, n - pos[i] + i);
sumcost += mincost;
}
printf("%lld\n", sumcost);
}
F - Three Paths on a Tree
题意:给一棵无权树,选三个点,使得他们之间的简单路径覆盖的边数最多。
题解:在树上上面选三个点,他们的路径必然也是一棵树,所以要求边数最多就是点数最多。其实很显然其中两个点必然是某一条直径的两端。首先,三个点都是叶子,因为若不是叶子,则把这个节点换成某个方向的叶子,一定更好。然后其中两个点必然是某一条直径的两端。否则,把其中两个点换成直径,会更好。所以只需要找出直径,然后找一个到直径距离最远的点。使用多源bfs即可。官方题解提到可以使用dp,仔细想想确实,设dp[i][j]为以1为根时,i的子树内选择j个点,覆盖的边的最大值,因为每次只会和当前子树的根有关所以满足dp的性质。简而言之:dp[u][0]=0,dp[u][1]=max(dp[v][1]+1),dp[u][2]=max(max(dp[v][2]+1),max(dp[v1][1]+dp[v2][1]+1)),dp[u][3]=max(max(dp[v][3]),max(dp[v1][2]+dp[v2][1]+1),max(dp[v1][1]+dp[v2][1]+dp[v3][1]+1))。
使用多源bfs,注意树为一条链的情况,此情况不存在“到直径距离最大且不不为0”的点,故把r3的初始值设为pa[r2],这样就算不更新答案也不会错。
int n;
vector<int> G[200005];
int pa[200005];
int dep[200005];
void dfs(int u, int p) {
pa[u] = p;
dep[u] = dep[p] + 1;
for(auto &v : G[u]) {
if(v == p)
continue;
dfs(v, u);
}
}
int r1, r2, r3;
queue<int> Q;
int dis[200005];
void bfs() {
while(!Q.empty())
Q.pop();
for(int i = 1; i <= n; ++i)
dis[i] = -1;
int ans = 0;
int x = r2;
r3 = pa[r2];
while(x) {
++ans;
Q.push(x);
dis[x] = 0;
x = pa[x];
}
while(!Q.empty()) {
int u = Q.front();
Q.pop();
for(auto &v : G[u]) {
if(dis[v] != -1)
continue;
dis[v] = dis[u] + 1;
Q.push(v);
}
}
for(int i = 1; i <= n; ++i) {
if(dis[i] > dis[r3])
r3 = i;
}
ans += dis[r3];
printf("%d\n%d %d %d\n", ans - 1, r1, r2, r3);
}
void test_case() {
scanf("%d", &n);
for(int i = 1; i <= n - 1; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1, 0);
r1 = 0;
for(int i = 1; i <= n; ++i) {
if(dep[i] > dep[r1])
r1 = i;
}
dfs(r1, 0);
r2 = 0;
for(int i = 1; i <= n; ++i) {
if(dep[i] > dep[r2])
r2 = i;
}
bfs();
}
反思:分类讨论要仔细。注意构造的几个点会不会发生重合,而且把答案的初始值设为某个“合理”答案而不是非法值“0”、“-1”,虽然在debug阶段可能不容易看出问题,但是假如真的是有问题的话,鲁棒性确实会好一些。