T1
沙漠點列
直接考慮貪心。容易發現首先一定是先割不在環上的邊,這種邊每割一條連通塊數量增加 \(1\)。然後考慮對環下手。要對一個環進行有用的操作,首先需要先割掉其上的一條邊,這次操作不產生貢獻。我們希望這樣的無用操作儘可能少,於是按照從大往小的順序割環即可。
程式碼
#include <iostream>
#include <algorithm>
using namespace std;
static char buf[1000005], *p1 = buf, *p2 = buf;
inline char nnc() { return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1000005, stdin), p1 == p2) ? EOF : *p1++; }
// #define nnc getchar
inline int read() {
int ret = 0, neg = 0;
char c = 0;
while (c < '0' || c > '9') c = nnc(), c == '-' ? (neg = 1) : 0;
while ('0' <= c && c <= '9') ret = ret * 10 + c - 48, c = nnc();
return ret * (neg ? -1 : 1);
}
int n, m, k;
int head[1000005], nxt[4000005], to[4000005], ecnt;
void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt; }
int dfn[1000005], low[1000005], ncnt;
int stk[1000005], sz;
int a[1000005], acnt, cnt2;
void tarjan(int x) {
dfn[x] = low[x] = ++ncnt;
stk[++sz] = x;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (!dfn[v]) {
tarjan(v);
low[x] = min(low[x], low[v]);
if (low[v] == dfn[x]) {
int t, c = 1;
do {
t = stk[sz--];
++c;
} while (t != v);
(c == 2) ? (++cnt2) : (a[++acnt] = c);
}
} else
low[x] = min(low[x], dfn[v]);
}
}
int main() {
freopen("desert.in", "r", stdin);
freopen("desert.out", "w", stdout);
n = read(), m = read(), k = read();
for (int i = 1; i <= m; i++) {
int u, v;
u = read(), v = read();
add(u, v);
add(v, u);
}
int ccnt = 0;
for (int i = 1; i <= n; i++) !dfn[i] ? (++ccnt, tarjan(i)) : void();
if (k <= cnt2)
cout << ccnt + k << "\n";
else {
ccnt += cnt2;
k -= cnt2;
sort(a + 1, a + acnt + 1, greater<int>());
for (int i = 1; i <= acnt; i++) {
if (k <= 1)
break;
--k;
ccnt += min(k, a[i] - 1);
k -= min(k, a[i] - 1);
}
cout << ccnt << "\n";
}
return 0;
}
T2
集合劃分
考慮到全集只有一個,那不持有全集的那個人必然有至少一位是全 \(0\) 的。也就是說必然存在一個位使得包含這個位的所有集合都被劃分給某個人。容易發現這個事情是遞迴的。然後考慮在第 \(i\) 次劃分時我們會將大小為 \(2^{n - i}\) 的集合劃分給一個人。那麼在給定 A 拿到的集合個數的情況下,容易知道有哪些次劃分是給 A 的。那麼每次劃分給 B 的時候就需要保證這一次劃分出的集合不能有 A 所要的。這個事情可以高位字首和維護。然後可以考慮 dp,\(dp[S]\) 表示是否存在合法方案將 \(S\) 集合中的位劃分完畢,轉移列舉上一次劃分的位即可。為了實現方便可以先列舉 popcount 以確定這次劃分是給誰,然後再列舉狀態轉移,雖然看起來可能比較蠢。
程式碼
#include <iostream>
#include <algorithm>
#include <cassert>
#include <vector>
using namespace std;
int n, m, K;
int a[300005];
int f[300005], g[300005], S;
int main() {
freopen("set.in", "r", stdin);
freopen("set.out", "w", stdout);
cin >> n >> m >> K;
S = (1 << n) - 1;
for (int i = 1, x; i <= m; i++) cin >> x, ++a[x];
for (int i = 0; i < n; i++) {
for (int j = 0; j < (1 << n); j++) {
if (j & (1 << i))
a[j] += a[j ^ (1 << i)];
}
}
f[0] = 1;
for (int i = n - 1; ~i; i--) {
for (int j = 0; j < (1 << n); j++) {
if (__builtin_popcount(j) != n - i)
continue;
if (K & (1 << i)) {
for (int k = 0; k < n; k++)
(j & (1 << k)) ? (f[j ^ (1 << k)] ? (g[j] = k, f[j] = 1) : 0) : 0;
} else {
for (int k = 0; k < n; k++)
(j & (1 << k)) ? (f[j ^ (1 << k)] && a[S ^ j] == a[S ^ j ^ (1 << k)] ? (g[j] = k, f[j] = 1) : 0) : 0;
}
}
}
if (!f[S])
return 0 * puts("-1");
string str;
for (int i = 0; i <= S; i++) str += '0';
for (int i = 0, x = S, t; i < n; i++) {
x ^= (1 << (t = g[x]));
if (K & (1 << i)) {
for (int j = 0; j <= S; j++) {
if ((j & (1 << t)) && (j & x) == 0)
str[j] = '1';
}
}
}
cout << str.substr(1) << "\n";
return 0;
}
T3
染色
首先觀察到答案具有上界 \(2(n - 1)\),即為先把腳下的染成下一格,然後走。然後考慮計算最多能省掉多少染色。發現在一個位置時可以選擇直接找到下一個和當前顏色一樣的位置,然後把這一段全染成一個顏色再走。會發現這實際上相當於要選出若干區間 \([l_i, r_i]\),滿足任意兩個區間不交於端點外的任何地方,要求最多選出多少區間。這個就隨便預處理然後倍增了。
程式碼
#include <iostream>
#include <string.h>
using namespace std;
static char buf[1000005], *p1 = buf, *p2 = buf;
inline char nnc() { return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1000005, stdin), p1 == p2) ? EOF : *p1++; }
// #define nnc getchar
inline int read() {
int ret = 0, neg = 0;
char c = 0;
while (c < '0' || c > '9') c = nnc(), c == '-' ? (neg = 1) : 0;
while ('0' <= c && c <= '9') ret = ret * 10 + c - 48, c = nnc();
return ret * (neg ? -1 : 1);
}
int n, q;
int a[1000005];
int lst[1000005];
int tor[20][1000005];
int l[1000005], r[1000005], ans[1000005];
int main() {
freopen("color.in", "r", stdin);
freopen("color.out", "w", stdout);
n = read(), q = read();
for (int i = 1; i <= n; i++) a[i] = read(), lst[i] = n + 1;
for (int i = n; i; i--) tor[0][i] = lst[a[i]], lst[a[i]] = i;
tor[0][n + 1] = tor[0][n] = n + 1;
for (int i = n; i; i--) tor[0][i] = min(tor[0][i], tor[0][i + 1]);
for (int i = 1; i <= 19; i++) {
for (int j = 1; j <= n + 1; j++)
tor[i][j] = tor[i - 1][tor[i - 1][j]];
}
for (int i = 1; i <= q; i++) l[i] = read(), r[i] = read(), (l[i] > r[i] ? swap(l[i], r[i]) : void()), ans[i] = 2 * (r[i] - l[i]);
for (int i = 19; ~i; i--) {
for (int _ = 1; _ <= q; _++) {
if (tor[i][l[_]] <= r[_])
ans[_] -= (1 << i), l[_] = tor[i][l[_]];
}
}
for (int i = 1; i <= q; i++) cout << ans[i] << "\n";
return 0;
}
T4
不跳棋
考慮不強制線上,可以直接時光倒流變加點,然後點分樹每個點上維護最小距離和次小距離和分別的個數即可。然後考慮強制線上的情況,相當於要在刪點的過程中維護最小和次小距離。在點分樹的每個點上維護一個子樹內點到它距離的桶,要刪點就考慮在每個點上維護兩個指標分別表示最小距離和次小距離,每次刪的時候移動兩個指標即可。不難注意到這倆玩意的移動都是單調的。在這裡兩個距離來自同一棵子樹並不會影響答案,因為全域性的答案一定會嚴格小於這兩個距離的貢獻。然後考慮對所有分治中心的答案開一個桶,在這個桶上也維護一個指標代表當前答案。由於每個分治中心的貢獻單調,則答案單調,因此這個指標的移動也是單調的。然後就做完了。
程式碼
#include <iostream>
#include <string.h>
#include <cassert>
#include <vector>
using namespace std;
static char buf[1000005], *p1 = buf, *p2 = buf;
inline char nnc() { return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1000005, stdin), p1 == p2) ? EOF : *p1++; }
// #define nnc getchar
inline int read() {
int ret = 0, neg = 0;
char c = 0;
while (c < '0' || c > '9') c = nnc(), c == '-' ? (neg = 1) : 0;
while ('0' <= c && c <= '9') ret = ret * 10 + c - 48, c = nnc();
return ret * (neg ? -1 : 1);
}
int n, tp;
int head[500005], nxt[1000005], to[1000005], ecnt;
inline void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt; }
int all, rt, rsz, Rt, asdf;
int sz[500005], msz[500005];
bool vis[500005];
int dist[19][500005], ddep[500005];
int df[500005];
long long cnt[500005], lans;
int ans = 1;
struct node {
vector<int> vec;
int ans, p, q, mxd;
long long cnt;
void init() {
p = 0, q = 1;
if (q <= mxd)
ans = 1, cnt = 1ll * vec[p] * vec[q];
else
ans = n + 1, cnt = 0;
::cnt[ans] += cnt;
}
void upd(int x) {
::cnt[ans] -= cnt;
--vec[x];
while (p <= mxd && !vec[p]) ++p;
while (q <= mxd && !(vec[q] - (p == q))) ++q;
if (q <= mxd)
ans = p + q, cnt = (p == q ? 1ll * vec[p] * (vec[p] - 1) / 2 : 1ll * vec[p] * vec[q]);
else
ans = n + 1, cnt = 0;
::cnt[ans] += cnt;
}
} a[500005];
vector<int> G[500005];
void getroot(int x, int fa, int d = 0, int X = 0) {
msz[x] = sz[x] = 1;
asdf = max(asdf, d);
X ? ++a[X].vec[d] : 0;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa && !vis[v]) {
getroot(v, x, d + 1, X);
sz[x] += sz[v];
msz[x] = max(msz[x], sz[v]);
}
}
msz[x] = max(msz[x], all - sz[x]);
if (msz[x] < rsz)
rsz = msz[x], rt = x;
}
void dfs1(int x) {
vis[x] = 1, asdf = 0;
getroot(x, 0);
a[x].vec.resize((a[x].mxd = asdf) + 1);
a[x].vec[0] = 1;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (!vis[v]) {
all = sz[v], rsz = n + 1;
getroot(v, x, 1, x);
ddep[rt] = ddep[df[rt] = x] + 1;
G[x].emplace_back(rt);
dfs1(rt);
}
}
}
void dfs3(int x, int fa, int X, int d = 0) {
dist[ddep[x] - ddep[X]][x] = d;
for (int i = head[x]; i; i = nxt[i]) (to[i] != fa && !vis[to[i]]) ? dfs3(to[i], x, X, d + 1) : void();
}
void dfs2(int x) { vis[x] = 1; dfs3(x, 0, x); for (auto v : G[x]) dfs2(v); }
void Del(int x) {
int u = x, c = 0;
while (u) {
a[u].upd(dist[c][x]);
u = df[u], ++c;
}
while (!cnt[ans]) ++ans;
}
signed main() {
n = read(), tp = read();
for (int i = 1; i < n; i++) {
int u, v;
u = read(), v = read();
add(u, v);
add(v, u);
}
all = n, rt = 0, rsz = n + 1;
getroot(1, 0);
dfs1(Rt = rt);
memset(vis, 0, sizeof vis);
dfs2(Rt);
for (int i = 1; i <= n; i++) a[i].init();
for (int i = 1; i <= n - 2; i++) {
Del(read() ^ (tp * lans));
cout << ans << " " << (lans = cnt[ans]) << "\n";
}
return 0;
}
構造:注意到某個構成單位的特殊性,考慮幹掉具有特殊性的構成單位,然後遞迴構造。