Codeforces Round #378 (Div. 2)
题目链接:https://codeforces.com/contest/733
A - Grasshopper And the String
题意:有个虫子,要跳过一个长度为 \(n\) 的字符串(从 \(0\) 跳到 \(n+1\) ),只能停在元音字母处。求最短的跳跃距离。
题解:求前后相邻两个停留位置的差的最大值。
char s[200005];
void test_case() {
scanf("%s", s + 1);
int n = strlen(s + 1);
int lst = 0, maxlen = 0;
for(int i = 1; i <= n; ++i) {
if(s[i] == 'A' || s[i] == 'E' || s[i] == 'I' || s[i] == 'O' || s[i] == 'U' || s[i] == 'Y') {
maxlen = max(maxlen, i - lst);
lst = i;
}
}
maxlen = max(maxlen, n + 1 - lst);
printf("%d\n", maxlen);
}
B - Parade
题意:阅兵,第 \(i\) 行的士兵有 \(l_i\) 个喜欢先出左脚,有 \(r_i\) 个喜欢先出右脚。定义阅兵的整齐度为 \(|L-R|\) ,其中 \(L=\sum_{i=1}^{n}l_i\) ,
\(R\) 同理。现在可以调整至多一个行的士兵,让他们左右颠倒,求最大的整齐度。
题解:枚举这个行。
int n;
int l[100005];
int r[100005];
void test_case() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d%d", &l[i], &r[i]);
ll sumL = 0, sumR = 0;
for(int i = 1; i <= n; ++i) {
sumL += l[i];
sumR += r[i];
}
int ans = 0;
ll cur = abs(sumL - sumR);
ll tsumL = sumL, tsumR = sumR;
for(int i = 1; i <= n; ++i) {
sumL -= l[i];
sumR -= r[i];
sumL += r[i];
sumR += l[i];
ll tmp = abs(sumL - sumR);
if(tmp > cur) {
ans = i;
cur = tmp;
}
sumL = tsumL;
sumR = tsumR;
}
printf("%d\n", ans);
}
*C - Epidemic in Monstropolis
题意:给一个数字序列 \(a\) ,规定每次可以选一个数字可以吃掉相邻的一个比它小的数字。再给一个数字序列 \(b\) ,求一种可以把 \(a\) 构造为 \(b\) 的方法。或者说明无解。
错误算法:数字比较少,只有 \(n(1\leq n\leq 500)\) 个。暗示可以搞立方的算法。首先每个 \(b_j\) 对应的范围是唯一确定的,然后每次规定这一段的最大的一个吃最小的一个数字,假如最大的和最小的相等就GG了。
题解:应该连续段内是不可以交换顺序的,所以按照敦爷的思路,枚举最后一个吃的数字,易知只有这个数字吃别的数字才是最好的,所以对于一个区间可以 \(len^2\) 枚举。
但是细节还是蛮多的,肯定是贪心取连续的一段 \(a_i\) 直至越界或超过 \(b_j\) ,假如此时不为 \(b_j\) 则无解。除此之外虽然考虑了 \(i\) 提前用完,但是没有考虑 \(j\) 提前用完,导致搞了几发。
int n, m;
int a[505], b[505];
bool check_help(int L, int R, int M) {
int l = M - 1, r = M + 1;
int sum = a[M];
while(l >= L || r <= R) {
if(l >= L && r <= R) {
if(a[l] <= a[r]) {
if(sum <= a[l])
return 0;
else {
sum += a[l];
--l;
}
} else {
if(sum <= a[r])
return 0;
else {
sum += a[r];
++r;
}
}
} else if(l >= L) {
if(sum <= a[l])
return 0;
else {
sum += a[l];
--l;
}
} else {
if(sum <= a[r])
return 0;
else {
sum += a[r];
++r;
}
}
}
return 1;
}
pair<int, char> ans[505];
int atop;
void gen(int L, int R, int M, int p) {
//printf("[%d,%d],%d\n", L, R, M);
int l = M - 1, r = M + 1;
int sum = a[M];
while(l >= L || r <= R) {
if(l >= L && r <= R) {
if(a[l] <= a[r]) {
sum += a[l];
--l;
ans[++atop] = {M - p, 'L'};
++p;
} else {
sum += a[r];
++r;
ans[++atop] = {M - p, 'R'};
}
} else if(l >= L) {
sum += a[l];
--l;
ans[++atop] = {M - p, 'L'};
++p;
} else {
sum += a[r];
++r;
ans[++atop] = {M - p, 'R'};
}
}
}
bool check(int L, int R, int p) {
for(int m = L; m <= R; ++m) {
if(check_help(L, R, m)) {
gen(L, R, m, p);
return 1;
}
}
return 0;
}
void test_case() {
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
scanf("%d", &m);
for(int j = 1; j <= m; ++j)
scanf("%d", &b[j]);
int i = 1, sum = 0;
atop = 0;
for(int j = 1; j <= m; ++j) {
if(i > n) {
puts("NO");
return;
}
int L = i, R = i;
while(i <= n && sum < b[j]) {
sum += a[i];
R = i;
++i;
}
if(sum != b[j]) {
puts("NO");
return;
}
if(!check(L, R, L - j)) {
puts("NO");
return;
}
sum = 0;
}
if(i <= n) {
puts("NO");
return;
}
puts("YES");
for(int i = 1; i <= atop; ++i)
printf("%d %c\n", ans[i].first, ans[i].second);
}
收获:只有移除左侧的元素才会使得中间元素的偏移值变大。注意观察一整段移除元素之后偏移值的变化。以及验证和构造可以分开写,加快常数也减少复杂度。
D - Kostya the Sculptor
题意:找至多两块可以完全贴合(某个面完全一致)的长方体,贴在一起,然后切一个最大的球出来。
题解:考虑要切的是球体,所以受限制的是最短边。所以可以把边排序,把长的两条边作为ID,然后尝试把短边贴在一起,这样有可能会改进答案。所以选个数据结构map就可以。甚至连常数都可以省掉,定义一个struct的前两维是长边,然后按照长边排序,可以贴合的就会排在附近,找出每种前两维相等的struct中最大的两个进行贴合。
int a[3];
map<pair<int, int>, pair<int, int> > M;
void test_case() {
int n;
scanf("%d", &n);
int ansL = 0;
int k = 0;
int k1 = 0, k2 = 0;
for(int i = 1; i <= n; ++i) {
scanf("%d%d%d", &a[0], &a[1], &a[2]);
sort(a, a + 3);
pii &tL = M[{a[1], a[2]}];
if(tL.first == 0) {
if(a[0] > ansL) {
ansL = a[0];
k = 1;
k1 = i;
}
tL = {a[0], i};
} else {
int curL = min(a[1], a[0] + tL.first);
if(curL > ansL) {
ansL = curL;
k = 2;
k1 = i;
k2 = tL.second;
}
if(a[0] > tL.first)
tL = {a[0], i};
}
}
printf("%d\n", k);
if(k == 1)
printf("%d\n", k1);
else
printf("%d %d\n", k1, k2);
}
*F - Drivers Dissatisfaction
群友们好像提到NOI魔法森林。
题意:有一个 \(n(2\leq n\leq 2\cdot 10^5)\) 点 \(m(n-1\leq m\leq 2\cdot 10^5)\) 边的连通无向边带权图。每条边除了权值 \(w_j\) 外还带有减少 \(w_j\) 一个单位的cost \(c_j\) 。用不超过 \(S\) 的cost,构造一棵mst。注意边权可以为负数或0。
题解:花钱减少的权值的边肯定是选一条既在mst里面的且单价最便宜的边。所以每条边事实上只有两种状态,原价或者最便宜价。那么枚举每条边作为最便宜的,然后这条边肯定必选,剩下的原价边求一次mst。这个复杂度是 \(m^2logm\) 。枚举的过程中有可能可以重复利用的就是原价的mst,所以怪不得是LCT。假如得到一棵原价mst,然后加入最便宜边的时候首先若是其原价边本身在mst中则直接计算新贡献即可,否则一定断开这条链上的原价最大值,然后连接最便宜边得到新的(可能的)mst。那么这个并不需要LCT,树剖也可以。但是其实树剖是可以带修改的,这道题不带修改,所以按道理用树剖的思路把一条链断成 \(logn\) 个连续区间,然后在ST表上面查就可以了。正好备一个树剖ST表。
看见是4秒,可以先写一个树剖线段树。但是最后还是直接树剖ST表过了。
需要注意的细节是,ST表需要传递最值来源于哪里。而且要临时“发明”一个对边树剖。注意对边树剖时,定下根之后除了根以外的每个节点和它与其父亲的边一一对应。在树剖上统计的时候,当在同一重链上时则不计算最高的节点,轻重链交替时要计算最高的节点。
#define lc (o<<1)
#define rc (o<<1|1)
const int MAXN = 200000 + 5;
int dep[MAXN], siz[MAXN], son[MAXN], fa[MAXN], top[MAXN], tid[MAXN], rnk[MAXN], cnt;
int n, m, r, mod;
int a[MAXN];
int head[MAXN], etop;
struct Edge {
int v, next;
int w, i;
} e[MAXN * 2];
inline void init(int n) {
etop = 0;
memset(head, -1, sizeof(head[0]) * (n + 1));
}
inline void addedge(int u, int v, int w, int i) {
e[++etop].v = v;
e[etop].w = w;
e[etop].i = i;
e[etop].next = head[u];
head[u] = etop;
e[++etop].v = u;
e[etop].w = w;
e[etop].i = i;
e[etop].next = head[v];
head[v] = etop;
}
pii faE[MAXN + 5];
struct SparseTable {
static const int MAXLOGN = 19;
static const int MAXN = 200000;
int n, logn[MAXN + 5];
pii f[MAXN + 5][MAXLOGN + 1];
void Init1() {
logn[1] = 0;
for(int i = 2; i <= MAXN; i++)
logn[i] = logn[i >> 1] + 1;
}
void Init2(int _n) {
n = _n;
for(int i = 1; i <= n; i++)
//树剖时应替换本身在线段树中build位置的东西
f[i][0] = faE[rnk[i]];
for(int j = 1, maxlogn = logn[n]; j <= maxlogn; j++) {
for(int i = 1; i + (1 << j) - 1 <= n; i++)
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
}
pii Query(int l, int r) {
//一个可以返回最大值以及最大值来源(最右)的ST表
int s = logn[r - l + 1];
return max(f[l][s], f[r - (1 << s) + 1][s]);
}
} st;
void init1() {
dep[r] = 1;
}
void dfs1(int u, int t, pii E) {
siz[u] = 1, son[u] = -1, fa[u] = t;
faE[u] = E;
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if(v == t)
continue;
dep[v] = dep[u] + 1;
dfs1(v, u, {e[i].w, e[i].i});
siz[u] += siz[v];
if (son[u] == -1 || siz[v] > siz[son[u]])
son[u] = v;
}
}
void init2() {
cnt = 0;
}
void dfs2(int u, int t) {
top[u] = t;
tid[u] = ++cnt;
rnk[cnt] = u;
if (son[u] == -1)
return;
dfs2(son[u], t);
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if(v == fa[u] || v == son[u])
continue;
dfs2(v, v);
}
}
pii CPTQuery(int u, int v) {
pii ret = {-INF, -INF};
int tu = top[u], tv = top[v];
while (tu != tv) {
if(dep[tu] > dep[tv]) {
swap(u, v);
tu = top[u], tv = top[v];
}
//对边树剖,没到同一重链的时候,要算上轻重链交错位置
ret = max(ret, st.Query(tid[tv], tid[v]));
v = fa[tv];
tv = top[v];
}
//对边树剖,已到达同一重链上,不再算上高处的节点对应的边,也就是左区间+1,越界需要返回
if(tid[u] == tid[v])
return ret;
if(dep[u] > dep[v])
swap(u, v);
ret = max(ret, st.Query(tid[u] + 1, tid[v]));
return ret;
}
struct DisjointSetUnion {
static const int MAXN = 200000;
int n, fa[MAXN + 5], rnk[MAXN + 5];
void Init(int _n) {
n = _n;
for(int i = 1; i <= n; i++) {
fa[i] = i;
rnk[i] = 1;
}
}
int Find(int u) {
int r = fa[u];
while(fa[r] != r)
r = fa[r];
int t;
while(fa[u] != r) {
t = fa[u];
fa[u] = r;
u = t;
}
return r;
}
bool Merge(int u, int v) {
u = Find(u), v = Find(v);
if(u == v)
return false;
else {
if(rnk[u] < rnk[v])
swap(u, v);
fa[v] = u;
rnk[u] += rnk[v];
return true;
}
}
} dsu;
struct E2 {
int w, c, u, v, i;
bool operator<(const E2 &e)const {
return w < e.w;
}
} e2[200005];
int ANSI[200005], ANSW[200005];
void test_case() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++i)
scanf("%d", &e2[i].w);
for(int i = 1; i <= m; ++i)
scanf("%d", &e2[i].c);
for(int i = 1; i <= m; ++i) {
scanf("%d%d", &e2[i].u, &e2[i].v);
e2[i].i = i;
}
//建立原价最小生成树
sort(e2 + 1, e2 + 1 + m);
dsu.Init(n);
init(n);
int cnt = 0;
ll sum = 0;
for(int i = 1; i <= m; ++i) {
if(dsu.Merge(e2[i].u, e2[i].v)) {
addedge(e2[i].u, e2[i].v, e2[i].w, e2[i].i);
sum += e2[i].w;
ANSI[++cnt] = e2[i].i;
ANSW[cnt] = e2[i].w;
if(cnt == n - 1)
break;
}
}
//对最小生成树进行树剖
init1();
int r = 1;
dfs1(r, -1, {-INF, -INF});
init2();
dfs2(r, r);
st.Init1();
st.Init2(n);
//统计答案
ll ans = sum;
int S, eid = -1, aid = -1;
scanf("%d", &S);
for(int j = 1; j <= m; ++j) {
pii ret = CPTQuery(e2[j].u, e2[j].v);
ll tmp = sum - ret.first + e2[j].w - S / e2[j].c;
if(tmp < ans) {
ans = tmp;
eid = ret.second;
aid = j;
}
}
printf("%lld\n", ans);
for(int j = 1; j <= n - 1; ++j) {
if(ANSI[j] != eid)
printf("%d %d\n", ANSI[j], ANSW[j]);
else
printf("%d %d\n", e2[aid].i, e2[aid].w - S / e2[aid].c);
}
return;
}