2024.10.24心有错杂题
Candies and Stones
来源:CF101E
首先 \(7.5s\) 可以让 \(O(nm)\) 通过。可以朴素 \(dp\) ,记录答案为 \(f\), 从哪里转移过来的为 \(g\)。然而空间只有 \(45MB\)。 考虑压空间,\(f\)滚掉就行了。\(g\) 由于转移只有两种, \(g\) 可变成 \(bitset\) 。但仍不够。直接每 \(B\) 行记录一个 \(f\), 块内算\(g\)。空间复杂度为 \(O(\frac{nm}{B}+\frac{mB}{\omega})\)。\(B\) 取 \(\sqrt{n\omega}\)最优
int n, f[M][N], p, x[N], y[N], m, B, dp[N], cur, tot;
bitset<N> g[K];
string ans;
int calc(int a, int b) {
return (x[a] + y[b]) % p;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> p;
FOR (i, 0, n - 1) {
cin >> x[i];
}
FOR (i, 0, m - 1) {
cin >> y[i];
}
B = sqrt(n * 64);
FOR (i, 1, m - 1) {
dp[i] = -inf;
}
FOR (i, 0, n - 1) {
FOR (j, 0, m - 1) {
if (j) {
dp[j] = max(dp[j - 1], dp[j]);
}
dp[j] += calc(i, j);
}
if (i % B == 0) {
FOR (j, 0, m - 1) {
f[i / B][j] = dp[j];
}
tot = max(tot, i / B);
}
}
cout << dp[m - 1] << '\n';
cur = m - 1;
ROF (i, tot, 0) {
int l = i * B, r = min(n - 1, (i + 1) * B);
FOR (j, 0, m - 1) {
dp[j] = f[i][j];
}
FOR (j, l + 1, r) {
FOR (k, 0, m - 1) {
if (k) {
if (dp[k - 1] > dp[k]) {
dp[k] = dp[k - 1];
g[j - l][k] = 1;
} else {
g[j - l][k] = 0;
}
} else {
g[j - l][k] = 0;
}
dp[k] += calc(j, k);
}
}
ROF (j, r, l + 1) {
while (g[j - l][cur]) {
ans += 'S';
--cur;
}
ans += 'C';
}
while (cur > 0 && f[i][cur] == f[i][cur - 1] + calc(l, cur)) {
--cur;
ans += 'S';
}
}
reverse(ans.begin(), ans.end());
cout << ans << '\n';
return 0;
}
Defender of Childhood Dream
来源:CF1583F
可以猜想答案为 \(\lceil log_{k}n \rceil\) 。考虑证明 \(c\) 种颜色可覆盖到 \(k^c\) 个点。采用数学归纳法。显然当 \(c=1\)时,最多只能到 \(k\) 个点。那么推广到 \(c+1\) 。将能用 \(c+1\) 染成的新图,所有点分成 \(k\) 个点集,使得这几个点集都能用 \(c\) 个颜色染成,然后点集之间用 \(c+1\) 去连就行了。 那么这 \(k\) 个点集最大都是 \(k^c\) 个,那么答案也就为 \(k^c \times k=k^{c+1}\)。为什么这种分法就是最多的呢?根据 \(dilworth\) 定理,最长链等于最小反链覆盖。反链也就是两两没有连边的点集。最长链点的个数为 \(k\),那么反链也就至多 \(k\) 个。至于构造,可以直接令 \(i\) 到 \(j\) 的颜色为 \(k\) 进制下 \(i \oplus j\) 的 \(highbit\)。与分成 \(k\) 个点集的过程类似。
int n, k, t[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> k;
int tmp = 1, ans = 0;
while (tmp < n) {
tmp *= k;
++ans;
}
cout << ans << '\n';
t[0] = 1;
FOR (i, 1, ans) {
t[i] = t[i - 1] * k;
}
FOR (i, 0, n - 1) {
FOR (j, i + 1, n - 1) {
ROF (w, ans - 1, 0) {
int a = (i / t[w]) % k, b = (j / t[w]) % k;
if (a != b) {
cout << w + 1 << ' ';
break;
}
}
}
}
return 0;
}
Flip the Cards
来源:CF1503D
很巧妙的一题。
首先容易发现若存在 \(a_i,b_i \le n\) 求无解。
那么剩余数一定一个 $ > n $ 一个 $ \le n$
直接将 \(>n\) 的那个数映射到小的上面。发现问题转化为花最小代价选出两个下降子序列。可以用数据结构维护,但是有更巧妙的解法。考虑对于 \(min_{j=1}^ia_j > max_{j=i+1}^na_j\)时,两段互不相干,那么直接搞出所有这样的 \(i\) 并且分段。惊喜地发现每段内分法唯一。这是因为你维护两个序列,一定有一个序列末尾时前缀最小值。那么来了一个新的数。若比前缀最小值大那么选法唯一。否则如果填到非前缀最小值那个序列末尾会发现后面一定有一个数比当前两个序列末尾都大,那这种数字就无法填了。所以也是唯一的。于是就做完了。
int n, f[N], a[N], b[N], ans, l0, l1, e0, e1, c0, c1, pre[N], suf[N];
bool flag[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
FOR (i, 1, n) {
cin >> a[i] >> b[i];
if (max(a[i], b[i]) <= n || min(a[i], b[i]) > n) {
cout << -1 << '\n';
return 0;
}
if (a[i] < b[i]) {
f[a[i]] = b[i];
} else {
f[b[i]] = a[i];
flag[b[i]] = 1;
}
}
pre[0] = inf;
FOR (i, 1, n) {
pre[i] = min(pre[i - 1], f[i]);
}
ROF (i, n, 1) {
suf[i] = max(suf[i + 1], f[i]);
}
e0 = inf;
e1 = inf;
FOR (i, 1, n) {
if (f[i] < e0) {
e0 = f[i];
++l0;
c0 += flag[i];
} else if (f[i] < e1) {
e1 = f[i];
++l1;
c1 += flag[i];
} else {
cout << -1 << '\n';
return 0;
}
if (pre[i] > suf[i + 1]) {
ans += min(c0 + l1 - c1, c1 + l0 - c0);
c0 = l0 = c1 = l1 = 0;
e0 = e1 = inf;
}
}
cout << ans << '\n';
return 0;
}
Joke
来源:Petrozavodsk Summer 2021. Day 3. IQ test J
首先将 \(p\) 排列成 \(1,2,...n\) 。然后 \(q\) 也对应地变换。接下来发现一个非常重要的性质 \(f(1,2,...n,q)\) 相当于 \(q\) 中上升子序列的个数。证明:按照 \(q\) 值从 \(1,2,...n\) 去填 \(s\)。 当位置 \(i\) 选择了 \(p_i < q_i\) 时,那么 \(i\) 左侧所有未填数的位置 \(j\) 必须要 \(p_j < q_j\)。如果 \(p_j>q_j\),会使得 \(q_i>p_i=i>p_j=j>q_j>q_i\),成环就没有合法的填数方法了。这样的话我们考虑对所有主动选择 \(p_i<q_i\)的选择的方案数去计数,这样能够唯一确定 \(s\)。容易发现不可能在 \(q_i<q_j,i>j\) 时 \(i,j\) 同时选。因为这样的话 \(i\) 选的时候 \(j\) 已经没有选择的余地了。那么这个条件等价于没有逆序对。即统计上升子序列个数。设 \(f_{i,j}\) 表示前 \(i\) 个数(要求 \(i\) 确定且 \(i\) 为子序列结尾), \(j\)个未知位置没有参与子序列的方案。转移的话直接枚举子序列中上一个确定的位置 \(k\) 然后枚举 \(k \sim i\) 中填了几个未知位置就行了。
int n, nq[N], p[N], q[N], f[N][N], C[N][N], cnt[N], s[N], fac[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
fac[0] = 1;
cin >> n;
FOR (i, 1, n) {
cin >> p[i];
fac[i] = fac[i - 1] * i % mod;
}
FOR (i, 1, n) {
cin >> q[i];
nq[p[i]] = q[i];
}
FOR (i, 1, n) {
q[i] = nq[i];
}
C[0][0] = 1;
FOR (i, 1, n) {
C[i][0] = 1;
FOR (j, 1, i) {
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
}
}
f[0][0] = 1;
FOR (i, 1, n) {
if (q[i]) {
cnt[q[i]] = 1;
}
}
FOR (i, 1, n) {
cnt[i] = 1 - cnt[i] + cnt[i - 1];
}
FOR (i, 1, n) {
s[i] = s[i - 1] + (q[i] == 0);
if (!q[i]) {
continue;
}
FOR (j, 0, s[i]) {
FOR (k, 0, i - 1) {
if (q[k] > q[i] || (!q[k] && k > 0)) {
continue;
}
int c = cnt[q[i]] - cnt[q[k]];
FOR (p, 0, min(c, s[i] - s[k])) {
f[i][j] += f[k][j - (s[i] - s[k]) + p] * C[c][p] % mod * C[s[i] - s[k]][p] % mod;
f[i][j] %= mod;
}
}
}
}
int ans = 0;
FOR (i, 0, n) {
if (i > 0 && !q[i]) {
continue;
}
FOR (j, 0, s[i]) {
FOR (k, 0, min(cnt[n] - cnt[q[i]], s[n] - s[i])) {
ans += fac[(s[n] - s[i]) - k + j] * f[i][j] % mod * C[s[n] - s[i]][k] % mod * C[cnt[n] - cnt[q[i]]][k] % mod;
ans %= mod;
}
}
}
cout << ans << '\n';
return 0;
}
连通图
来源:[AHOI2013]
显然可以大力 \(LCT\) 或者 线段树分治,哈希更为巧妙。
考虑以下做法。跑出 \(dfs\)树,令每条非树边权值为 \(2^i\)。然后将树上两点之间所有的边的权值都异或上 \(2^i\)。那么每次询问只要判断割边的子集异或和是否为 \(0\) 即可。
首先证明割的异或为 \(0\)。令每个点的权值为所有连着它的边的权值的异或和。显然所有点的异或和为 \(0\)。若图被分为了 \(A\) , \(B\) 两个连通块。那么显然割的异或也就是 \(A\) 中所有点的异或和,也就是 \(0\)。
接下来证明异或为 \(0\) 的一定是割。假设不是割。那么将询问中所有的选一条非树边成为树边,该树边成为非树边,容易得到此时仍然满足条件,且与所有非树边的权值线性无关。与异或和为 \(0\) 矛盾。
于是就证完了。
因为 \(2^i\) 不是很好算所以就直接替换为随机权值就行。
int n, m, val[N], q, k, c, s[N], x[N], y[N], b[N];
mt19937_64 rnd(time(NULL));
vector<PII> G[N];
bool vis[N];
void dfs(int u, int fa) {
vis[u] = 1;
for (auto [v, id]: G[u]) {
if (v == fa) {
continue;
}
if (vis[v]) {
val[id] = rnd();
} else {
dfs(v, u);
}
}
}
void calc(int u, int fa) {
vis[u] = 1;
int fid = 0;
for (auto [v, id]: G[u]) {
if (v == fa) {
fid = id;
continue;
}
if (vis[v]) {
continue;
}
calc(v, u);
s[u] ^= s[v];
}
val[fid] = s[u];
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
FOR (i, 1, m) {
cin >> x[i] >> y[i];
G[x[i]].PB(MP(y[i], i));
G[y[i]].PB(MP(x[i], i));
}
dfs(1, 0);
FOR (i, 1, m) {
if (!val[i]) {
continue;
}
s[x[i]] ^= val[i];
s[y[i]] ^= val[i];
}
memset(vis, 0, sizeof(vis));
calc(1, 0);
cin >> q;
FOR (i, 1, q) {
cin >> k;
bool flag = 0;
FOR (j, 1, k) {
cin >> c;
int tmp = val[c];
ROF (w, 60, 0) {
if (!((tmp >> w) & 1)) {
continue;
}
if (!b[w]) {
b[w] = tmp;
break;
}
tmp ^= b[w];
}
if (!tmp) {
flag = 1;
}
}
FOR (w, 0, 60) {
b[w] = 0;
}
if (!flag) {
cout << "Connected\n";
} else {
cout << "Disconnected\n";
}
}
return 0;
}
Squares
来源:CF1495F
首先集合 \(S\) 的加减直接转化为 \(O(q)\) 次查询最短路。首先 \([x,y]\) 的最大值的位置一定会经过,分成两半。容易求出 \(F_i\) 表示 \(i\) 位置走到右边第一个 \(p_j>p_i\) 的点的最小花费,\(G_i\) 表示左边第一个 \(p_j>p_i\) 走到 \(i\) 的最小花费。计算前缀和。查询的时候倍增出最大值因为一定在 \(l \sim r\) 以及 \(l\) 一直向右跳第一个大于它的数,\(r\) 一直向左跳第一个大于它的数的路径上,直接根据前缀和算就行了。有点牛。
int n, f[N], g[N], nxt[N][20], prv[N], st[N], top, p[N], a[N], b[N], q;
set<int> s;
int calc(int l, int r) {
if (r == n + 1) {
return f[l];
}
int cur = l;
ROF (i, 19, 0) {
if (nxt[cur][i] && nxt[cur][i] <= r) {
cur = nxt[cur][i];
}
}
return f[l] - f[cur] + g[r] - g[cur];
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> q;
FOR (i, 1, n) {
cin >> p[i];
}
FOR (i, 1, n) {
cin >> a[i];
}
FOR (i, 1, n) {
cin >> b[i];
}
p[0] = p[n + 1] = inf;
st[top = 1] = n + 1;
ROF (i, n, 1) {
int sum = a[i];
while (top && p[st[top]] < p[i]) {
sum += f[st[top]];
--top;
}
f[i] = min(sum, b[i]);
nxt[i][0] = st[top];
st[++top] = i;
}
st[top = 1] = 0;
nxt[n + 1][0] = n + 1;
FOR (i, 1, n) {
int lst = 0;
while (top && p[st[top]] < p[i]) {
lst = st[top--];
}
g[i] = ((st[top] == i - 1) ? a[i - 1] : (g[lst] + f[lst]));
prv[i] = st[top];
st[++top] = i;
}
FOR (i, 1, n) {
g[i] += g[prv[i]];
}
ROF (i, n, 1) {
f[i] += f[nxt[i][0]];
}
FOR (i, 1, 19) {
FOR (j, 1, n + 1) {
nxt[j][i] = nxt[nxt[j][i - 1]][i - 1];
}
}
int ans = calc(1, n + 1);
s.insert(1);
s.insert(n + 1);
FOR (i, 1, q) {
int u;
cin >> u;
if (u != 1) {
if (s.find(u) != s.end()) {
int a = *(--s.find(u));
int b = *(++s.find(u));
s.erase(u);
ans += calc(a, b) - calc(a, u) - calc(u, b);
} else {
s.insert(u);
int a = *(--s.find(u));
int b = *(++s.find(u));
ans -= calc(a, b) - calc(a, u) - calc(u, b);
}
}
cout << ans << '\n';
}
return 0;
}
Outermost Maximums
来源:CF1693E
有点意思。
有个显然的结论是对于 \(a_i\)。一定能够安排操作顺序使得它变成 \(min(max_{j=1}^ia_j,max_{j=i+1}^na_j)\) 。这样的话直接从大往小枚举值域,维护每个出现过的数最后一次操作是向左,向右,还是随便左和右结果都一样(分别对应 \(<-,->,?\) )。模拟一些样例,会发现一个符合直觉的事情就是 \(<-\) 都在左边, \(->\) 都在右边, \(?\) 都在中间。那么直接维护当前值域的最前和最后位置,根据原来的 \(?\) 区间去分类讨论得出新的 \(?\) 区间,最后再用树状数组计算 \(?\) 区间内出现过的数,直接求和就做完了。
int n, l[N], r[N], a[N], bit[N], ans, L, R;
vector<int> pos[N];
void upd(int x) {
while (x <= n) {
bit[x]++;
x += x & -x;
}
}
int qry(int x) {
int res = 0;
while (x) {
res += bit[x];
x -= x & -x;
}
return res;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
FOR (i, 1, n) {
l[i] = inf;
}
FOR (i, 1, n) {
cin >> a[i];
l[a[i]] = min(l[a[i]], i);
r[a[i]] = max(r[a[i]], i);
pos[a[i]].PB(i);
}
ROF (i, n, 1) {
if (l[i] > r[i]) {
continue;
}
if (!L && !R) {
L = l[i];
R = r[i];
} else if (R < l[i]) {
L = R + 1;
R = r[i];
} else if (L > r[i]) {
R = L - 1;
L = l[i];
} else {
L = l[i];
R = r[i];
}
ans += qry(R) - qry(L - 1);
for (int x: pos[i]) {
upd(x);
}
}
cout << ans + qry(n) << '\n';
return 0;
}
Security System
来源:CF79E
挺唐氏的题。注意到只有四个角会有贡献,直接贪心。不知道咋*2900的。
int n, x[4], y[4], t, a, b, c, w[4];
int s(int sx, int sy, int tx, int ty) {
int dis = abs(sx - tx) + abs(sy - ty);
return dis * (dis + 1) / 2;
}
int f(int id, int sx, int sy) {
int ans = s(x[id], y[id], sx, sy) + s(n, n, x[id], y[id]);
if (sx <= x[id] && sy <= y[id]) {
return ans;
}
if (sx <= x[id] && sy > y[id]) {
return ans - s(x[id], sy, x[id], y[id]) * 2 + abs(sy - y[id]);
}
if (sx > x[id] && sy <= y[id]) {
return ans - s(x[id], y[id], sx, y[id]) * 2 + abs(sx - x[id]);
}
return ans - 2 * s(sx, sy, x[id], y[id]) + abs(sx - x[id]) + abs(sy - y[id]);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> t >> a >> b >> c;
FOR (i, 0, 3) {
x[i] = a + (c - 1) * (i & 1);
y[i] = b + (c - 1) * (i & 2) / 2;
}
int cx = 1, cy = 1;
FOR (i, 0, 3) {
if (f(i, 1, 1) - abs(x[i] - 1) - abs(y[i] - 1) > t) {
cout << "Impossible" << '\n';
return 0;
}
}
FOR (i, 1, 2 * n - 2) {
if (cx == n) {
cout << "U";
++cy;
continue;
}
if (cy == n) {
cout << "R";
++cx;
continue;
}
bool fl = 1;
FOR (j, 0, 3) {
if (w[j] + f(j, cx + 1, cy) > t) {
fl = 0;
break;
}
}
if (fl) {
cout << "R";
++cx;
} else {
cout << "U";
++cy;
}
FOR (j, 0, 3) {
w[j] += abs(cx - x[j]) + abs(cy - y[j]);
}
}
return 0;
}
AmShZ wins a Bet
来源:CF1610G
首先发现一个关键的性质,就是删除的字符串一定是连续的。证明的话显然当 \(i,j\) 括号对内部有括号没删干净,将右括号换掉一定不劣,若没有右括号将左括号换成连续的也不变。因此,倒序 \(dp\) 求出离 \(i\) 最近的合法右端点 \(nxt_i\) 。\(f_i = min(S_i+f_{i+1},f_{nxt_i})\) 随便 \(hash\) 优化一下,然后就做完了。其实没那么难关键在于发现性质。细节有点多。
int n, nxt[N], cur = 3e5, pos[N << 1], to[N][20], lst, fst[N];
ull h[N][20], pw[N];
bool f[N];
string s;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> s;
n = s.SZ;
s = " " + s;
pw[0] = 1;
FOR (i, 1, N - 5) {
pw[i] = pw[i - 1] * P;
}
pos[cur] = n + 1;
lst = n + 1;
ROF (i, n, 1) {
if (s[i] == '(') {
++cur;
} else {
--cur;
}
nxt[i] = pos[cur];
pos[cur] = i;
fst[i] = fst[nxt[i]];
int p = fst[i];
int rp = to[p][0];
if (s[i] == '(' && nxt[i]) {
if (!p || s[p] != s[i]) {
if (p && s[p] > s[i]) {
f[i] = 1;
}
} else {
int cx = rp, cy = fst[i + 1];
ROF (j, 18, 0) {
if (h[cx][j] == h[cy][j]) {
cx = to[cx][j];
cy = to[cy][j];
}
}
if (cy > n || (cx <= n && s[cy] < s[cx])) {
f[i] = 1;
}
}
} else {
f[i] = 1;
}
if (!f[i]) {
continue;
}
fst[i] = i;
to[i][0] = fst[i + 1];
h[i][0] = s[i] == ')';
FOR (j, 1, 18) {
h[i][j] = h[i][j - 1] * pw[1 << j - 1] + h[to[i][j - 1]][j - 1];
to[i][j] = to[to[i][j - 1]][j - 1];
}
}
int cur = fst[1];
while (cur > 0 && cur != n + 1) {
cout << s[cur];
cur = to[cur][0];
}
cout << '\n';
return 0;
}
Assigning Fares
来源:CF1322F