Codeforces Round #782 (Div. 2) VP 记录
赛时 4 题,E 是赛后补的 /kk
A. Red Versus Blue
题目保证了 \(b < r\),那么也就是说 \(B\) 把整个序列分成了 \(B+1\) 份,然后每份中填入尽可能少的 \(R\) 。
最优的方式显然是尽可能去均匀分配,每份填入 \(\frac{R}{B+1}\) 个 \(R\),如果有剩下的 \(R\) 没填,那每份多填一个即可。
void Main() {
n = read(), R = read(), B = read();
int p = R / (B + 1), q = R % (B + 1);
for(int j = 1; j <= p + (1 <= q); ++j) cout << "R";
for(int i = 1; i <= B; ++i) {
cout << "B";
for(int j = 1; j <= p + (i + 1 <= q); ++j) cout << "R";
}
puts("");
}
signed main() {
T = read();
while(T--) Main();
return 0;
}
B. Bit Flipping
反向考虑,每个位置都会被异或 \(K\) 次,每次选中一个位置就是让它少异或一次。
从前向后贪心看看能不能让尽可能多的位置异或成 \(1\) 即可,多余的操作次数全扔到最后一位。
void Main() {
n = read(), K = read(); int lst = K;
for(int i = 1; i <= n; ++i) scanf("%1d", &a[i]), cnt[i] = 0;
for(int i = 1; i <= n; ++i) {
if((K & 1) && a[i] && lst) lst--, cnt[i] = 1;
if(!(K & 1) && !a[i] && lst) lst--, cnt[i] = 1;
}
if(lst) cnt[n] += lst;
for(int i = 1; i <= n; ++i) printf("%d", a[i] ^ ((K - cnt[i]) & 1)); puts("");
for(int i = 1; i <= n; ++i) printf("%d ", cnt[i]); puts("");
}
signed main() {
T = read();
while(T--) Main();
return 0;
}
C. Line Empire
第一眼可以设一个 \(f_{i,j}\) 表示基地在第 \(j\) 个位置,并且走完了前 \(i\) 个位置的最小花费。
不过这样转移是 \(n^3\) 的,显然过不了。
考虑这个题目自身的性质,发现如果基地最后走到了第 \(i\) 个位置,那么在占领第 \(i\) 个位置之前都可以把基地移动到第 \(i-1\) 个位置然后再去占领第 \(i\) 个位置,而对于第 \(i\) 个之后的位置都只能从第 \(i\) 个位置出发去占领。也就是说如果我们确定了最后基地最后走到了哪个位置,那么整个过程的最优方案我们也是确定的。
所以我们只需要枚举基地最后在哪个位置,然后通过预处理一些需要的东西就可以直接 \(O(1)\) 算出贡献了。
void Main() {
n = read(), A = read(), B = read(), sum[n + 1] = 0;
for(int i = 1; i <= n; ++i) a[i] = read();
for(int i = n; i >= 1; --i) sum[i] = sum[i + 1] + a[i];
int ans = INF;
for(int i = 0; i <= n; ++i) ans = min(ans, A * a[i] + B * a[i] + B * (sum[i + 1] - a[i] * (n - i)));
cout << ans << "\n";
}
signed main() {
T = read();
while(T--) Main();
return 0;
}
D. Reverse Sort Sum
可以从前往后考虑这个位能不能填 \(1\)。
如果之前填了 \(x\) 个 \(1\),当前考虑到第 \(i\) 位。
那么在这里填 \(1\) 之后,会对 \([i-x,n-x]\) 这段区间有一个 \(1\) 的贡献,并且在这之前这个 \(1\) 还会对 \(i\) 这个位置产生 \(i-1\) 次贡献。
区间加减操作直接上线段树。
假设这里填 \(1\),如果减去它产生的贡献后发现最小值出现了负数,说明这里应该填 \(0\),否则说明这里可以填 \(1\)。
正确性大概就是每个位置 \(1\) 的贡献都是越来越靠后的?就是说后面的 \(1\) 不会对前面的造成贡献,所以可以从前向后确定。
namespace Seg {
#define lson i << 1
#define rson i << 1 | 1
int Min[MAXN << 2], lazy[MAXN << 2];
void Push_up(int i) { Min[i] = min(Min[lson], Min[rson]); }
void Build(int i, int l, int r) {
lazy[i] = Min[i] = 0;
if(l == r) return Min[i] = a[l], void();
int mid = (l + r) >> 1;
Build(lson, l, mid), Build(rson, mid + 1, r);
Push_up(i);
}
void Push_down(int i) {
if(!lazy[i]) return ;
lazy[lson] += lazy[i], lazy[rson] += lazy[i];
Min[lson] -= lazy[i], Min[rson] -= lazy[i];
lazy[i] = 0;
}
void Modify(int i, int l, int r, int L, int R, int val) {
if(L <= l && r <= R) return lazy[i] += val, Min[i] -= val, void();
Push_down(i); int mid = (l + r) >> 1;
if(mid >= L) Modify(lson, l, mid, L, R, val);
if(mid < R) Modify(rson, mid + 1, r, L, R, val);
Push_up(i);
}
int Query(int i, int l, int r, int L, int R) {
if(L <= l && r <= R) return Min[i];
Push_down(i); int mid = (l + r) >> 1, ans = INF;
if(mid >= L) ans = min(ans, Query(lson, l, mid, L, R));
if(mid < R) ans = min(ans, Query(rson, mid + 1, r, L, R));
return ans;
}
}
void Main() {
n = read();
for(int i = 1; i <= n; ++i) a[i] = read();
Seg::Build(1, 1, n);
int lst = 0;
for(int i = 1; i <= n; ++i) {
int L = i - lst, R = i - lst + n - i;
Seg::Modify(1, 1, n, L, R, 1);
Seg::Modify(1, 1, n, i, i, i - 1);
int p = Seg::Query(1, 1, n, L, R);
int q = Seg::Query(1, 1, n, i, i);
if(p >= 0 && q >= 0) {
b[i] = 1, lst ++;
} else {
b[i] = 0;
Seg::Modify(1, 1, n, L, R, - 1);
Seg::Modify(1, 1, n, i, i, - i + 1);
}
}
for(int i = 1; i <= n; ++i) cout << b[i] << " "; puts("");
}
signed main() {
T = read();
while(T--) Main();
return 0;
}
E. AND-MEX Walk
胡乱手模可以发现答案只能是 \(0,1,2\),因为做 \(\&\) 前缀和的话 \(1,2\) 不能同时出现。
看到位运算想到拆位。因为点可以重复经过,所以可以往联通块的方向上去想。
考虑答案为 \(0\) 的情况,就是存在一条 \(u \to v\) 的路径,这个路径上的所有边至少有一位全部都为 \(1\)。
那么我们根据边的权值,对于每一位用一个并查集,如果边 \((u,v)\) 的 \(w\) 的这一位为 \(1\),那么可以在并查集中将 \(u,v\) 合并。
那答案为 \(0\) 的情况就变成了判断是否存在一位的并查集中 \(u,v\) 属于同一个连通块。
考虑答案为 \(1\) 的情况。首先要建立在答案不是 \(0\) 之前。
然后,我们要在 \(\&\) 前缀和变为 \(0\) 之前不能出现让其 \(1\),我们可以在先保证二进制中的某一位都为 \(1\) 之前,先把二进制中的第一位的 \(1\) 消掉。
那么,对于所有边权为偶数的边,对每一位所在的连通块打上标记。
如果存在一位中 \(u\) 这个点的联通块被打了标记,那么说明上面的条件可以做到,也就是说答案为 \(1\)。
否则答案为 \(0\)。
struct node { int u, v, w; }e[MAXN];
int n, m, Q;
struct Graph {
int fa[MAXN];
bool vis[MAXN];
void Init() { for(int i = 1; i <= n; ++i) fa[i] = i; }
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
void Add(int x, int y) {
int uf = find(x), vf = find(y);
if(uf != vf) fa[uf] = vf;
}
bool Query(int x, int y) { return find(x) == find(y); }
}G[31];
signed main() {
n = read(), m = read();
for(int i = 0; i <= 30; ++i) G[i].Init();
for(int i = 1, u, v, w; i <= m; ++i) {
e[i].u = read(), e[i].v = read(), e[i].w = read();
for(int j = 0; j <= 30; ++j) {
if((e[i].w >> j) & 1) {
G[j].Add(e[i].u, e[i].v);
}
}
}
for(int i = 1; i <= m; ++i) {
if(e[i].w & 1) continue;
for(int j = 1; j <= 30; ++j) {
G[j].vis[G[j].find(e[i].u)] = true;
G[j].vis[G[j].find(e[i].v)] = true;
}
}
Q = read();
for(int i = 1, u, v; i <= Q; ++i) {
u = read(), v = read();
bool flag = false;
for(int j = 0; j <= 30; ++j) flag |= (G[j].Query(u, v));
if(flag) { puts("0"); continue; }
for(int j = 1; j <= 30; ++j) flag |= G[j].vis[G[j].find(u)];
flag ? puts("1") : puts("2");
}
return 0;
}