HEOI2016/TJOI2016 做题笔记
HEOI2016/TJOI2016 做题笔记
题目:https://www.luogu.com.cn/problem/list?keyword=&tag=33%7C95&page=1
[HEOI2016/TJOI2016] 树
直接树剖,不断往上查询,通过 set 和记录重链上最上面的标记,不难做到 \(O(n \log n)\)。
ケロシの代码
const int N = 1e5 + 5;
int n, m;
int fi[N], ne[N << 1], to[N << 1], ecnt;
int fa[N], sz[N], son[N];
int dfn[N], rnk[N], top[N], cnt;
set<int> S[N]; int f[N];
void add(int u, int v) {
ne[++ ecnt] = fi[u];
to[ecnt] = v;
fi[u] = ecnt;
}
void dfs1(int u) {
sz[u] = 1;
son[u] = - 1;
for(int i = fi[u]; i; i = ne[i]) {
int v = to[i];
if(v == fa[u]) continue;
fa[v] = u;
dfs1(v);
sz[u] += sz[v];
if(son[u] == - 1 || sz[v] > sz[son[u]]) son[u] = v;
}
}
void dfs2(int u, int tp) {
dfn[u] = ++ cnt;
rnk[cnt] = u;
top[u] = tp;
if(son[u] != - 1) dfs2(son[u], tp);
for(int i = fi[u]; i; i = ne[i]) {
int v = to[i];
if(v == fa[u] || v == son[u]) continue;
dfs2(v, v);
}
}
void modify(int u) {
S[top[u]].insert(dfn[u]);
chmin(f[top[u]], dfn[u]);
}
int query(int u) {
while(u) {
if(f[top[u]] <= dfn[u])
return rnk[* prev(S[top[u]].upper_bound(dfn[u]))];
u = fa[top[u]];
}
}
void solve() {
cin >> n >> m;
REP(_, n - 1) {
int u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
dfs1(1); dfs2(1, 1);
FOR(i, 1, n) f[i] = n + 1;
modify(1);
REP(_, m) {
char opt; cin >> opt;
if(opt == 'C') {
int u; cin >> u;
modify(u);
}
else {
int u; cin >> u;
cout << query(u) << endl;
}
}
}
[HEOI2016/TJOI2016] 排序
由于只有一个查询,不难想到二分答案,把原序列变成 01 序列,然后就好维护了,使用线段树模拟即可,时间复杂度 \(O(n \log^2 n)\)。
ケロシの代码
const int N = 1e5 + 5;
int n, m, a[N], b[N], p;
struct Modify {
int o, l, r;
} q[N];
struct SgT {
int le[N << 2], ri[N << 2];
int F[N << 2], T[N << 2];
void pushup(int u) {
F[u] = F[u << 1] + F[u << 1 | 1];
}
void push(int u, int x) {
F[u] = x * (ri[u] - le[u] + 1);
T[u] = x;
}
void pushdown(int u) {
if(T[u] != - 1) {
push(u << 1, T[u]);
push(u << 1 | 1, T[u]);
T[u] = - 1;
}
}
void build(int u, int l, int r) {
le[u] = l, ri[u] = r;
T[u] = - 1;
if(l == r) {
F[u] = b[l];
return;
}
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u);
}
void modify(int u, int l, int r, int x) {
if(l <= le[u] && ri[u] <= r) {
push(u, x);
return;
}
pushdown(u);
int mid = le[u] + ri[u] >> 1;
if(l <= mid) modify(u << 1, l, r, x);
if(mid < r) modify(u << 1 | 1, l, r, x);
pushup(u);
}
int query(int u, int l, int r) {
if(l <= le[u] && ri[u] <= r) {
return F[u];
}
pushdown(u);
int mid = le[u] + ri[u] >> 1;
int res = 0;
if(l <= mid) res += query(u << 1, l, r);
if(mid < r) res += query(u << 1 | 1, l, r);
return res;
}
} t;
bool check(int mid) {
FOR(i, 1, n) b[i] = a[i] > mid;
t.build(1, 1, n);
FOR(i, 1, m) {
int c1 = t.query(1, q[i].l, q[i].r);
int c0 = q[i].r - q[i].l + 1 - c1;
if(q[i].o) {
if(c1) t.modify(1, q[i].l, q[i].l + c1 - 1, 1);
if(c0) t.modify(1, q[i].r - c0 + 1, q[i].r, 0);
}
else {
if(c0) t.modify(1, q[i].l, q[i].l + c0 - 1, 0);
if(c1) t.modify(1, q[i].r - c1 + 1, q[i].r, 1);
}
}
return t.query(1, p, p) == 0;
}
void solve() {
cin >> n >> m;
FOR(i, 1, n) cin >> a[i];
FOR(i, 1, m) cin >> q[i].o >> q[i].l >> q[i].r;
cin >> p;
int L = 1, R = n, ans = 1;
while(L <= R) {
int mid = L + R >> 1;
if(check(mid)) {
ans = mid;
R = mid - 1;
}
else {
L = mid + 1;
}
}
cout << ans << endl;
}
[HEOI2016/TJOI2016] 序列
首先对于最长不降子序列,肯定想到 dp。但是这题的转移条件比较复杂,是 \(\max a'_j \le a_i\) 且 \(a_j \le \min a'_i\)。
但是不难发现这是一个二维偏序,直接分治做,算左半边对右半边的贡献即可,使用树状数组维护,时间复杂度 \(O(n \log^2 n)\)。
ケロシの代码
const int N = 1e5 + 5;
int n, m;
int a[N], f[N], g[N];
int dp[N];
vector<PII> eq, em;
struct fenwick {
int c[N];
int lowbit(int x) {
return - x & x;
}
void modify(int u, int x) {
for(int i = u; i <= n; i += lowbit(i))
chmax(c[i], x);
}
void clear(int u) {
for(int i = u; i <= n; i += lowbit(i))
c[i] = 0;
}
int query(int u) {
int res = 0;
for(int i = u; i; i -= lowbit(i))
chmax(res, c[i]);
return res;
}
} t;
void slv(int l, int r) {
if(l == r) return;
int mid = l + r >> 1;
slv(l, mid);
em.clear(); eq.clear();
FOR(i, l, mid) em.push_back({f[i], i});
FOR(i, mid + 1, r) eq.push_back({a[i], i});
sort(ALL(em)); sort(ALL(eq));
int pl = 0;
for(auto h : eq) {
while(pl < SZ(em) && FI(em[pl]) <= FI(h)) {
int u = SE(em[pl]);
t.modify(a[u], dp[u] + 1);
pl ++;
}
int u = SE(h);
chmax(dp[u], t.query(g[u]));
}
FOR(i, l, mid) t.clear(a[i]);
slv(mid + 1, r);
}
void solve() {
cin >> n >> m;
FOR(i, 1, n) cin >> a[i];
FOR(i, 1, n) f[i] = g[i] = a[i];
REP(_, m) {
int u, x;
cin >> u >> x;
chmax(f[u], x);
chmin(g[u], x);
}
FOR(i, 1, n) dp[i] = 1;
slv(1, n);
int ans = 0;
FOR(i, 1, n) chmax(ans, dp[i]);
cout << ans << endl;
}
[HEOI2016/TJOI2016] 游戏
发现限制是行和列不能有重复的,自然想到通过行和列建二分图,然后跑最大匹配。
当时发现题目中有硬石头,所以直接对每行每列分没有硬石头的连续段,作为一个点即可。
直接用 Dinic 跑二分图最大匹配即可,时间复杂度 \(O(nm\sqrt{nm})\)。
ケロシの代码
namespace Dinic {
const int N = 2e3 + 5;
const int M = 1e4 + 5;
const int INF = 0x3f3f3f3f;
struct Edge {
int ne, to, ew;
} e[M];
int fi[N], c[N], ecnt;
int S, T;
int d[N];
void init() {
memset(fi, 0, sizeof fi);
ecnt = 1;
}
void add(int u, int v, int w) {
e[++ ecnt] = {fi[u], v, w};
fi[u] = ecnt;
e[++ ecnt] = {fi[v], u, 0};
fi[v] = ecnt;
}
bool bfs() {
memset(d, 0x3f, sizeof d);
queue<int> q;
d[S] = 0; q.push(S);
while(! q.empty()) {
int u = q.front();
q.pop();
for(int i = fi[u]; i; i = e[i].ne) if(e[i].ew) {
int v = e[i].to;
if(d[v] == INF) {
d[v] = d[u] + 1;
q.push(v);
}
}
}
return d[T] != INF;
}
int dfs(int u, int w) {
if(u == T || ! w) return w;
int res = 0;
for(int & i = c[u]; i; i = e[i].ne) {
int v = e[i].to;
if(d[v] != d[u] + 1) continue;
int val = dfs(v, min(w, e[i].ew));
if(! val) continue;
e[i].ew -= val;
e[i ^ 1].ew += val;
res += val;
w -= val;
if(! w) return res;
}
return res;
}
int dinic(int _S, int _T) {
S = _S, T = _T;
int res = 0;
while(bfs()) {
memcpy(c, fi, sizeof c);
res += dfs(S, INF);
}
return res;
}
}
const int N = 55;
int n, m;
int f[N][N], t1;
int g[N][N], t2;
string s[N];
void solve() {
cin >> n >> m;
FOR(i, 1, n) {
cin >> s[i];
s[i] = ' ' + s[i];
}
FOR(i, 1, n) FOR(j, 1, m) if(s[i][j] != '#') {
if(j == 1 || s[i][j - 1] == '#') f[i][j] = ++ t1;
else f[i][j] = f[i][j - 1];
}
FOR(j, 1, m) FOR(i, 1, n) if(s[i][j] != '#') {
if(i == 1 || s[i - 1][j] == '#') g[i][j] = ++ t2;
else g[i][j] = g[i - 1][j];
}
Dinic :: init();
int S = 0, T = t1 + t2 + 1;
FOR(i, 1, t1) Dinic :: add(S, i, 1);
FOR(i, 1, t2) Dinic :: add(t1 + i, T, 1);
FOR(i, 1, n) FOR(j, 1, m) if(s[i][j] == '*')
Dinic :: add(f[i][j], t1 + g[i][j], 1);
cout << Dinic :: dinic(S, T) << endl;
}
[HEOI2016/TJOI2016] 字符串
对于 LCP 问题,考虑后缀数组。
但是这道题有右端点的限制,所以每次询问都二分答案,那么合法后缀的左端点区间即为 \([a,b-mid+1]\)。
接下来需要找这个区间内找到 LCP 大于等于 \(mid\) 的后缀左端点。
不难发现 \(LCP(i,j)=\min_{k=rk_i+1}^{rk_j} height_k\),所以 LCP 在 \(rk\) 上是单峰的,所以只需找到与 \(rk_c\) 两边最近的两个端点即可。
直接使用主席树维护即可。
时间复杂度 \(O(n \log^2 n)\)。
ケロシの代码
const int N = 1e5 + 5;
const int M = 2e7 + 5;
int n, m;
string s;
int len, sa[N], rk[N], a[N], tp[N], h[N];
int lg[N], f[N][19];
int rt[N], tot, ls[M], rs[M], F[M];
void Sort() {
FOR(i, 1, len) a[i] = 0;
FOR(i, 1, n) a[rk[i]] ++;
FOR(i, 1, len) a[i] += a[i - 1];
ROF(i, n, 1) sa[a[rk[tp[i]]] --] = tp[i];
}
void SA() {
len = 130;
FOR(i, 1, n) rk[i] = s[i], tp[i] = i;
Sort();
for(int w = 1, p = 0; w <= n; len = p, p = 0, w <<= 1) {
FOR(i, n - w + 1, n) tp[++ p] = i;
FOR(i, 1, n) if(sa[i] > w) tp[++ p] = sa[i] - w;
Sort(); swap(rk, tp);
p = rk[sa[1]] = 1;
FOR(i, 2, n) rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + w] == tp[sa[i - 1] + w]) ? p : ++ p;
if(p == n) return;
}
}
void build() {
FOR(i, 2, n) lg[i] = lg[i >> 1] + 1;
FOR(i, 1, n) f[i][0] = h[i];
FOR(j, 1, lg[n]) FOR(i, 1, n - (1 << j) + 1)
f[i][j] = min(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
}
int get(int l, int r) {
int len = lg[r - l + 1];
return min(f[l][len], f[r - (1 << len) + 1][len]);
}
int lcp(int l, int r) {
if(l == r) return n - sa[l] + 1;
if(l > r) swap(l, r);
int len = lg[r - l ++];
return min(f[l][len], f[r - (1 << len) + 1][len]);
}
int insert(int u, int l, int r, int p) {
int v = ++ tot;
ls[v] = ls[u];
rs[v] = rs[u];
F[v] = F[u] + 1;
if(l == r) {
return v;
}
int mid = l + r >> 1;
if(p <= mid) ls[v] = insert(ls[u], l, mid, p);
else rs[v] = insert(rs[u], mid + 1, r, p);
return v;
}
int query_l(int u, int v, int l, int r, int p) {
if(p < l || F[v] - F[u] == 0) {
return - 1;
}
if(l == r) {
return l;
}
int mid = l + r >> 1;
int val = query_l(rs[u], rs[v], mid + 1, r, p);
if(val == - 1) val = query_l(ls[u], ls[v], l, mid, p);
return val;
}
int query_r(int u, int v, int l, int r, int p) {
if(r < p || F[v] - F[u] == 0) {
return - 1;
}
if(l == r) {
return l;
}
int mid = l + r >> 1;
int val = query_r(ls[u], ls[v], l, mid, p);
if(val == - 1) val = query_r(rs[u], rs[v], mid + 1, r, p);
return val;
}
bool check(int l1, int r1, int l, int mid) {
int pos = rk[l];
int pl = query_l(rt[l1 - 1], rt[r1], 1, n, pos);
int pr = query_r(rt[l1 - 1], rt[r1], 1, n, pos);
if(pl != 1 && lcp(pl, pos) >= mid) return 1;
if(pr != 1 && lcp(pr, pos) >= mid) return 1;
return 0;
}
void solve() {
cin >> n >> m;
cin >> s; s = ' ' + s;
SA();
int p = 0;
FOR(i, 1, n) {
if(p) p --;
int j = sa[rk[i] - 1];
while(i + p <= n && j + p <= n && s[i + p] == s[j + p]) p ++;
h[rk[i]] = p;
}
build();
FOR(i, 1, n) rt[i] = insert(rt[i - 1], 1, n, rk[i]);
REP(_, m) {
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
int L = 1, R = min(r1 - l1 + 1, r2 - l2 + 1), ans = 0;
while(L <= R) {
int mid = L + R >> 1;
if(check(l1, r1 - mid + 1, l2, mid)) {
ans = mid;
L = mid + 1;
}
else {
R = mid - 1;
}
}
cout << ans << endl;
}
}
[HEOI2016/TJOI2016] 求和
对于 \(i<j\),有 \(\begin{Bmatrix} i \\j \end{Bmatrix} = 0\),所以直接推式子:
推出来的式子一边只和 \(k\) 有关,一边只和 \(j-k\) 有关,直接卷积即可。
时间复杂度 \(O(n \log n)\)。
ケロシの代码
const int N = 4e5 + 5;
const int P = 998244353;
int add(int x, int y) { return (x + y < P ? x + y : x + y - P); }
void Add(int & x, int y) { x = (x + y < P ? x + y : x + y - P); }
int sub(int x, int y) { return (x < y ? x - y + P : x - y); }
void Sub(int & x, int y) { x = (x < y ? x - y + P : x - y); }
int mul(int x, int y) { return (1ll * x * y) % P; }
void Mul(int & x, int y) { x = (1ll * x * y) % P; }
int fp(int x, int y) {
int res = 1;
for(; y; y >>= 1) {
if(y & 1) Mul(res, x);
Mul(x, x);
}
return res;
}
int n, fac[N], finv[N];
int r[N], F[N], G[N];
void NTT(int * a, int lim, int o) {
REP(i, lim) if(i < r[i]) swap(a[i], a[r[i]]);
for(int i = 1; i < lim; i <<= 1) {
int wn = fp(o == 1 ? 3 : (P + 1) / 3, (P - 1) / (i << 1));
for(int j = 0; j < lim; j += (i << 1)) {
for(int k = 0, w = 1; k < i; k ++, Mul(w, wn)) {
int x = a[j + k];
int y = mul(w, a[j + k + i]);
a[j + k] = add(x, y);
a[j + k + i] = sub(x, y);
}
}
}
if(o == 1) return;
int inv = fp(lim, P - 2);
REP(i, lim) Mul(a[i], inv);
}
void solve() {
cin >> n;
int lim = 1, l = 0;
while(lim < (n << 1)) lim <<= 1, l ++;
REP(i, lim) r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));
fac[0] = 1;
FOR(i, 1, n) fac[i] = mul(fac[i - 1], i);
finv[n] = fp(fac[n], P - 2);
ROF(i, n - 1, 0) finv[i] = mul(finv[i + 1], i + 1);
FOR(i, 0, n) F[i] = finv[i];
FOR(i, 0, n) if(i % 2) F[i] = sub(0, F[i]);
FOR(i, 0, n) G[i] = mul(sub(fp(i, n + 1), 1), mul(finv[i], fp(sub(i, 1), P - 2)));
G[1] = n + 1;
NTT(F, lim, 1);
NTT(G, lim, 1);
REP(i, lim) Mul(F[i], G[i]);
NTT(F, lim, - 1);
int ans = 0;
FOR(i, 0, n) Add(ans, mul(F[i], mul(fac[i], fp(2, i))));
cout << ans << endl;
}