做题乱记
P3177
tags:树形 dp
对于每个子树,维护的是该子树内的所有边对答案做出的贡献。
从 \(f_{v,q}\) 转移到 \(f_{u,p}\),那么这条边权为 \(w\) 被经过的次数就是 \(q\times (k-q)+(siz_v-q)\times(n-k-(siz_v-q))\),乘上 \(w\) 进行转移即可。
记得上下界优化以及合法性的检测。
void dfs (int u, int fa) {
siz[u] = 1;
for (auto [v, w] : gra[u]) {
if (v == fa || siz[v]) continue;
dfs (v, u); siz[u] += siz[v];
for (int j = min (siz[u], k); ~j; j --)
for (int p = max (0, j - siz[u] + siz[v]); p <= min (siz[v], j); p ++)
f[u][j] = max (f[u][j], f[u][j - p]
+ f[v][p] +
w * 1ll * ( 1ll * (siz[v] - p) * (n - k - siz[v] + p) + p * 1ll * (k - p) ) );
}
}
CF486D Vaild Sets
tags: 计数、树形dp
一眼秒了。
考虑一个计数顺序:枚举每个点作为最小值,计算大于等于这个值的方案。当一个点存在集合中且点权与最小值相同时,当且仅当其编号大于初始点。
void dfs (int u, int fa, int root) {
f[u] = 1;
for (auto v : g[u]) {
if (v == fa) continue;
if ((a[v] > a[root] || a[v] == a[root] && v > root) && a[v] - a[root] <= d)
dfs (v, u, root),
fprintf (stderr, "%d -> %d availible\n", u, v),
f[u] = f[u] * (f[v] + 1) % p;
}
if (u == root) ans = (ans + :: f[u]) % p;
}
int main (void) {
scanf ("%d%d", &d, &n);
rep (i, 1, n) scanf ("%d", a + i);
rep (i, 1, n - 1) {
int u, v; scanf ("%d%d", &u, &v);
g[u].push_back (v), g[v].push_back (u);
}
for (int i = 1; i <= n; i ++) dfs (i, -1, i);
printf ("%lld\n", ans);
return 0;
}
P5666 [CSP-S2019] 树的重心
tags: 树的重心、乱搞
Data: \(n\le 3\times10^5\)。
考虑每个点作为重心的次数 \(f_i\)。不妨以原树的重心 \(g\) 为根,那么一个点在被原树被分割后为重心,分割的边必然不在其子树内。
假设当前点 \(u\not=g\) 的重儿子大小为 \(k\),\(siz_u=s\)。一条符合条件的边连向子节点的子树大小为 \(x\)。
那么 \(k\) 必然不大于新树节点个数的一半,即
同时,\(u\) 的新子树(即与父节点相连)的节点个数不超过新节点个数的一半,即
于是,一个充要条件就是:
下面讨论 \(u=g\) 的情况。
设 \(g\) 的重儿子大小为 \(S_g\),次重儿子(并不严格)大小为 \(S_p\),割掉的边失去的子树大小为 \(q\)。
如果这条边在重儿子上,那么 \(S_g-q\) 与 \(S_p\) 需要小于新树的大小 \(n'=n-q\) 的一半,即:
也就是:
如果不在重儿子上,那么只需使新树大小的一半不小于 \(S_g\),即:
综上,用树状数组维护即可。
由于我比较唐,不会什么精妙的维护方法,索性就分类讨论。
- 对于在 \(g\to u\) 路径上的点 \(v\),\(siz\) 为 \(n-siz_v\),这个部分可以直接 dfs 跑一遍维护,设这个值为 \(c_{1,v}\)。
- 对于其他点(不在子树内,不在 \(g\to u\) 的路径上),\(siz\) 为 \(siz_v\),记这个值为 \(c_{2,v}\)。
对于 \(u\),作为重心次数就是 \(c_{1,u}+c_{2,u}\)。
但是第二个不好直接维护啊!
那就用容斥的思想!
我们先把所有点的 \(siz=siz_v\) 求出当作初始系数。减掉子树内这样的点,这是好维护的,将计算子树后减去子树之前的查询结果就行。这样还会多算 \(g\to u\) 的 \(siz=siz_v\),和第一个方法一样减去就好。
void dfs1 (int u, int fa) {
siz[u] = 1;
for (auto v : gra[u]) {
if (v == fa) continue;
dfs1 (v, u); siz[u] += siz[v];
mxsiz[u] = max (mxsiz[u], siz[v]);
}
mxsiz[u] = max (mxsiz[u], n - siz[u]);
if (mxsiz[u] < mxsiz[g]) g = u;
}
void dfs (int u) {
mxsiz[u] = 0; siz[u] = 1;
dfn[u] = ++ tim;
for (auto v : gra[u]) {
if (v == fa[u]) continue;
fa[v] = u;
dfs (v); siz[u] += siz[v];
if (siz[v] >= mxsiz[u]) {
pmxsiz[u] = mxsiz[u];
mxsiz[u] = siz[v];
son[u] = v;
} else pmxsiz[u] = max (pmxsiz[u], siz[v]);
}
}
long long ans = 0;
void del_subtree (int u) {
for (auto v : gra[u]) {
if (v == fa[u]) continue ;
coef[v] = t.sum (n - 2 * siz[v], n - 2 * mxsiz[v]);
del_subtree (v);
coef[v] -= t.sum (n - 2 * siz[v], n - 2 * mxsiz[v]);
t.add (siz[v], 1);
}
}
void add_rtt (int u) {
for (auto v : gra[u]) {
if (v == fa[u]) continue;
t.add (n - siz[v], 1);
coef[v] += t.sum (n - 2 * siz[v], n - 2 * mxsiz[v]);
add_rtt (v);
t.add (n - siz[v], -1);
}
}
void del_vrtt (int u) {
for (auto v : gra[u]) {
if (v == fa[u]) continue;
t.add (siz[v], 1);
coef[v] -= t.sum (n - 2 * siz[v], n - 2 * mxsiz[v]);
del_vrtt (v);
t.add (siz[v], -1);
}
}
void add_all (int u) {
for (auto v : gra[u]) {
if (v == fa[u]) continue;
t.add (siz[v], 1);
add_all (v);
}
}
void get_son (int u) {
if (u == g) {
int v = son[u];
if (siz[v] <= n - 2 * pmxsiz[g]) ans += g;
get_son (v);
return ;
}
for (auto v : gra[u]) {
if (v == fa[u]) continue;
if (siz[v] <= n - 2 * pmxsiz[g]) ans += g;
get_son (v);
}
}
void get_other (int u) {
for (auto v : gra[u]) {
if (v == fa[u] || v == son[g]) continue;
if (siz[v] <= n - 2 * mxsiz[g]) ans += g;
get_other (v);
}
}
void work () {
init (); ans = 0;
scanf ("%d", &n);
for (int i = 1; i < n; i ++) {
int u, v; scanf ("%d %d", &u, &v);
gra[u].push_back (v), gra[v].push_back (u);
}
dfs1 (1, 0);
dfs (g); t.init (n);
del_subtree (g); t.init (n); add_rtt (g);
del_vrtt (g);
add_all (g);
rep (i, 1, n) {
if (i == g) continue;
coef[i] += t.sum (n - 2 * siz[i], n - 2 * mxsiz[i]);
ans += coef[i] * 1ll * i;
}
get_son (g);
get_other (g);
printf ("%lld\n", ans);
}
P4219 [BJOI2014] 大融合
tags: 树剖、离线思想
把问答离线下来,然后就能把树的形态刻画出来。
跑一遍 dfs,就能确定祖孙关系。
对于询问而言,实际上就是动态维护每个点子树大小。把询问逆向,每次加边相当于在树中删边,这是可以用树剖维护的。答案就是一端的 \(siz\) 乘上另一端的 \(siz\)。
再加一下细节。
要维护每个点当前的根,但是修改的时候发现修改子树会导致已经不在当前树内的还被修改。
注意一点:删边必然会导致当前子树根的深度增大,因此对于根的修改取一下最大值即可。
时间复杂度感觉还行,因为你查询根之类是 \(\mathcal O(\log n)\) 级别的,一次查询调用次数比较少。瓶颈还是在树剖上,所以复杂度是 \(\mathcal O(n\log^2n)\)
#include <array>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int n, q;
vector <int> g[N];
vector < array <int, 3> > qry;
int siz[N], fa[N], son[N], top[N], dep[N], id[N], tim[N], Time, rt[N];
void dfs (int u) {
siz[u] = 1;
dep[u] = dep[ fa[u] ] + 1;
for (auto v : g[u]) {
if (fa[u] == v) continue;
fa[v] = u; rt[v] = rt[u];
dfs (v);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v;
}
}
void dfs (int u, int tp) {
tim[u] = ++ Time; id[Time] = u;
top[u] = tp;
if (son[u]) dfs (son[u], tp);
for (auto v : g[u]) if (!tim[v]) dfs (v, v);
}
#define ls (u << 1)
#define rs (u << 1 | 1)
#define mid (l + r >> 1)
int sum[N << 2], root[N << 2], tag[N << 2], rtag[N << 2];
inline int comp (int u, int v) { if (dep[u] > dep[v]) return u; return v; }
void upd (int u, int k) { sum[u] += k; tag[u] += k; }
void updrt (int u, int rt) { root[u] = comp (root[u], rt); rtag[u] = root[u]; }
void pushdown (int u) { int &v = tag[u]; int &rt = rtag[u]; upd (ls, v), upd (rs, v); updrt (ls, rt), updrt (rs, rt); v = rt = 0; }
void modify (int l, int r, int ql, int qr, int k, int u = 1) {
if (ql > r || l > qr) return ;
if (l >= ql && r <= qr) return upd (u, k);
pushdown (u);
modify (l, mid, ql, qr, k, ls);
modify (mid + 1, r, ql, qr, k, rs);
}
void rt_modify (int ql, int qr, int k, int l = 1, int r = n, int u = 1) {
if (ql > r || l > qr) return ;
if (l >= ql && r <= qr) return updrt (u, k);
pushdown (u);
rt_modify (ql, qr, k, l, mid, ls);
rt_modify (ql, qr, k, 1 + mid, r, rs);
}
int query (int l, int r, int q, int u = 1) {
if (l == r) return sum[u];
pushdown (u);
if (q <= mid) return query (l, mid, q, ls);
else return query (mid + 1, r, q, rs);
}
int getRt (int q, int l = 1, int r = n, int u = 1) {
if (l == r) return root[u];
pushdown (u);
if (q <= mid) return getRt (q, l, mid, ls);
return getRt (q, mid + 1, r, rs);
}
void path_modify (int u, int v, int k) {
while (top[u] != top[v]) {
if (dep[ top[u] ] < dep[ top[v] ]) swap (u, v);
modify (1, n, tim[ top[u] ], tim[u], k);
u = fa[ top[u] ];
}
if (dep[u] > dep[v]) swap (u, v);
modify (1, n, tim[u], tim[v], k);
}
void test_Output () {
for (int i = 1; i <= n; i ++) {
fprintf (stderr, "%d: siz = %d, root = %d\n", i, query (1, n, tim[i]), getRt (tim[i]));
}
}
int main (void) { dep[0] = -1;
scanf ("%d%d\n", &n, &q);
for (int i = 1; i <= q; i ++) {
char opt[10]; int a, b;
scanf ("%s%d%d\n", opt + 1, &a, &b);
qry.push_back ({opt[1] == 'A', a, b});
if (opt[1] == 'A') g[a].push_back (b), g[b].push_back (a);
}
for (int i = 1; i <= n; i ++) if (!fa[i]) { rt[i] = i; dfs (i); dfs (i, i);}
for (int i = 1; i <= n; i ++) modify (1, n, tim[i], tim[i], siz[i]), rt_modify (tim[i], tim[i], rt[i]);
reverse (qry.begin (), qry.end ()); vector <long long> ans;
for (auto [typ, u, v] : qry) {
if (typ == 1) {
if (dep[u] > dep[v]) swap (u, v);
path_modify (getRt (tim[v]), u, -query (1, n, tim[v]));
rt_modify (tim[v], tim[v] + siz[v] - 1, v);
} else {
if (dep[u] > dep[v]) swap (u, v);
int rt = getRt (tim[u]),
d1 = query (1, n, tim[rt]) - query (1, n, tim[v]),
d2 = query (1, n, tim[v]);
ans.push_back (d1 * 1ll * d2);
}
}
reverse (ans.begin (), ans.end ());
for (auto as : ans) printf ("%lld\n", as);
return 0;
}
P4216 [SCOI2015] 情报传递
tags: 树剖、离线思想、主席树
在线:实际上是动态查询一段上值小于 \(C\) 的个数,在主席树上维护就做完了!
离线:另一种想法是,把询问离线下来,那么每个询问都有一个时间限制,这个时间限制实质上就是形如 \(c\) 时刻前开始的都可以被统计。我们按照这个方法排序,然后就发现可以直接做了。随便什么简单的数据结构维护一下就好。
正常做是树剖,\(\mathcal O(n\log^2n)\)。但可以维护这样的:\(s[i]\) 表示 \(i\) 到根有多少需要统计的点。修改的话就是对 \(u\) 的整个子树加,查询就是做一次差分。可以做到 \(\mathcal O(n\log n)\)
你应该在九月八日在 15min 以内完成这道题。
int main (void) {
scanf ("%d", &n);
for (int i = 1; i <= n; i ++) {
scanf ("%d", &par[i][0]);
if (!par[i][0]) root = i;
else g[ par[i][0] ].push_back (i);
} dfs (root);
int q; scanf ("%d", &q);
for (int i = 1; i <= q; i ++) {
int k; scanf ("%d", &k); ans[i] = -1;
int x, y, c;
if (k == 1) scanf ("%d%d%d", &x, &y, &c), qry.push_back ({i - c, x, y, i});
else scanf ("%d", &x), qry.push_back ({i, x, 0, i});
} t.init (n);
sort (qry.begin (), qry.end (), [&](auto x, auto y){ return x[0] == y[0] ? x[2] > y[2] : x[0] < y[0]; });
for (auto [c, a, b, id] : qry) {
if (b) {
int _lca = lca (a, b);
ans[id] = t.sum (dfn[a]) + t.sum (dfn[b]) - t.sum (dfn[ par[_lca][0] ]) - t.sum (dfn[_lca]);
ans1[id] = dep[a] + dep[b] - dep[_lca] - dep[ par[_lca][0] ];
} else t.add (dfn[a], 1), t.add (dfn[a] + siz[a], -1);
}
for (int i = 1; i <= q; i ++)
if (ans[i] != -1) printf ("%d %d\n", ans1[i], ans[i]);
return 0;
}
P5773 [JSOI2016] 轻重路径
tags: 树的重心、倍增
神仙题啊!
一次修改只会影响到当前叶子节点到根的这一条链。
考虑这条链上可能被修改的点。假设当前子树 \(rt\) 的大小为 \(siz_{rt}\),那么 \(rt\) 的后代 \(u\) 可能变为轻儿子必须满足 \(siz_{u}\le \dfrac{siz_{rt}}2\)。倍增找 \(siz_u\) 的 \(u\),判断是否改变。
由于每次 \(siz\) 至少减半,因此计算了至多 \(\log n\) 次。
void dfs (int u) {
dep[u] = dep[ par[u][0] ] + 1;
L[u] = dfn[u] = ++ tim;
id[tim] = u;
siz[u] = 1;
for (int i = 1; i < 19; i ++) par[u][i] = par[ par[u][i - 1] ][i - 1];
for (auto v : son[u])
if (v) par[v][0] = u, dfs (v), siz[u] += siz[v];
if (siz[ son[u][1] ] > siz[ son[u][0] ]) wson[u] = son[u][1];
else wson[u] = son[u][0];
ans += wson[u];
R[u] = tim;
}
int Size (int u) {
if (!u) return 0;
return t.sum (dfn[u], dfn[u] + siz[u] - 1);
}
int brot (int u) {
int f = par[u][0];
return son[f][!cha[u]];
}
int main (void) {
scanf ("%d", &n); t.init (n); memset (cha, -1, sizeof cha);
for (int i = 1; i <= n; i ++) scanf ("%d%d", &son[i][0], &son[i][1]), cha[ son[i][0] ] = 0, cha[ son[i][1] ] = 1, t.add (i, 1);
dfs (1); printf ("%lld\n", ans);
scanf ("%d", &q);
while (q --) {
int u, rt = 1; scanf ("%d", &u);
t.add (dfn[u], -1);
int l = u;
while (rt != l) {
u = l;
for (int i = 18; ~i; i --)
if (dep[ par[u][i] ] > dep[rt] && Size (par[u][i]) <= Size (rt) / 2) u = par[u][i];
int bro = brot (u), fa = par[u][0];
if (wson[fa] == u) {
int siu = Size (u), sib = Size (bro);
if (siu < sib) wson[fa] = bro, ans += bro - u;
if (!siu && !sib) ans -= u, wson[fa] = 0;
}
rt = u;
}
printf ("%lld\n", ans);
}
return 0;
}
CF1528C Trees of Tranquillity
tags: dfs 序,set 应用,贪心
神仙题啊!
由于 \(i>fa_i\),所以可以将 Keshi 树映射到 dfs 序上。这样,点 \(u\) 管辖的范围 \(rg_u\) 即 \([dfn_u,dfn_u+siz_u-1]\)。对于 Soroush 树上一条链上的两点 \(i<j\),若 \(i,j\) 可以同时选,当且仅当 \(rg_i\) 与 \(rg_j\) 无交。在 dfs Soroush 树时,可以直接从父节点继承选了的点集,进行分类讨论:
- 若当前点覆盖了已有的点,当前点不选。
- 若有点覆盖了当前点,删除那个点。
这样保证了单调性,且管辖区间范围逐渐缩小,所以贪心是正确的。
具体实现可以用 set。
void dfs (const vector <int>* g, int u) {
l[u] = ++ tim;
for (auto v : g[u]) dfs (g, v);
r[u] = tim;
}
set < pair <int, int> > s;
void dfs2 (const vector <int>* g, int u) {
auto it = s.lower_bound ({l[u], r[u]});
bool flag = false;
pair <int, int> del = {0, 0};
if (it == s.end ()) {
if (s.empty ())
s.insert ({l[u], r[u]}), flag = true;
else {
it --;
if (it -> second < l[u]) s.insert ({l[u], r[u]}), flag = true;
else if (it -> second >= r[u]) {
del = *it; s.erase (del); flag = true;
s.insert ({l[u], r[u]});
}
}
}
else {
if (it == s.begin ()) {
if (r[u] < it -> first)
s.insert ({l[u], r[u]}),
flag = true;
} else {
auto p = it; p --;
if (p -> second < l[u]) s.insert ({l[u], r[u]}), flag = true;
else if (p -> second >= r[u]) {
del = *p; s.erase (del);
flag = true;
s.insert ({l[u], r[u]});
}
}
}
ans = max (ans, (int)s.size ());
for (auto v : g[u]) dfs2 (g, v);
if (flag) s.erase ({l[u], r[u]});
if (del.first != 0) s.insert (del);
}
void work () {
init ();
scanf ("%d", &n);
for (int i = 2; i <= n; i ++) scanf ("%d", f1 + i), g1[ f1[i] ].push_back (i);
for (int i = 2; i <= n; i ++) scanf ("%d", f2 + i), g2[ f2[i] ].push_back (i);
dfs (g2, 1);
dfs2 (g1, 1);
printf ("%d\n", ans);
}
CF2063E Triangle Tree
tags:推式子、LCA
假设两边长度为 \(a,b\ \ (a\le b)\),那么 \(x\in(b-a,a+b)\),也就是有 \(2a-1\) 个选择。
把这个东西放在树上,假设 \(u,v\) 两点 LCA 为 \(l\)。那么就是有 \(2\min(dep_u,dep_{v})-2dep_l-1\) 个选择。考虑每个点 \(u\) 作为 LCA 的贡献,对于在同一个子节点内的子树的重复统计,我们额外记录总共有多少个这样的对。将 \(dep\) 从小到大排序,假设子树大小为 \(n\),使 \(dep_i\) 被计算的有 \(n-i\) 个。
好像明白了。继续拆贡献,由于 \(2\min(a,b)=a+b-\lvert a-b \rvert\),于是对于两点 \(u,v\) 的贡献 \(f(u,v)=dep_u+dep_v-\lvert dep_u-dep_v \rvert-2dep_{\operatorname{lca}(u,v)}-1\)。发现这个是十分好处理的。对于 \(dep_u+dep_v-2dep_{\operatorname{lca}(u,v)}\),把每个点当作 LCA 的贡献算一下,维护每个子树的 \(dep\) 和 \(s_u\),那么贡献就是 \(\dfrac12\sum\limits_{v\in son_u}s_v\times(siz_u-siz_v)\)。对于 \(\lvert dep_u-dep_v\rvert\),这也是好算的。在求出每个点的深度之后,将整个数组从小到大排序,维护后缀和。对于 \(-1\),和第一个是一样的。
然后就做完了。
void dfs (int u) {
siz[u] = 1; ++ :: cnt[dep[u] = dep[ fa[u] ] + 1];
ll cnt = 0;
ans += (n - 1) * 1ll * dep[u];
for (auto v : g[u]) {
if (v == fa[u]) continue;
fa[v] = u;
dfs (v);
cnt += siz[v] * 1ll * (siz[u] - 1);
siz[u] += siz[v];
}
ans -= 2 * (cnt + siz[u] - 1) * dep[u],
ans += siz[u] - 1;
}
void work () {
init ();
scanf ("%d", &n);
for (int i = 1; i < n; i ++) {
int u, v; scanf ("%d%d", &u, &v);
g[u].push_back (v); g[v].push_back (u);
}
dfs (1);
ll suf = 0;
for (int i = 1; i <= n; suf += cnt[i ++]) ans -= i * 1ll * cnt[i] * (suf - n + cnt[i] + suf);
printf ("%lld\n", ans - n * 1ll * (n - 1) / 2);
}
[ARC088F] Christmas Tree
tags:树形dp、二分、set 的应用
考虑 A 怎么做。
设 \(f(x)\) 表示以 \(u\) 为根的子树的答案,且向 \(fa_x\) 连了一条边。若有偶数个子节点,那么 \(f(x)=\sum\limits_{v\in son_x}f(v)-\dfrac{\lvert son_x\rvert}2+1\);若有奇数个子节点,那么 \(f(x)=\sum\limits_{v\in son_x}f(v)-\lfloor\dfrac{\lvert son_x\rvert}{2}\rfloor\),对于根节点特判一下即可。
接下来考虑 B 怎么算。注意到答案单调,因此考虑二分。观察 A 的做法,类似的进行判断即可。
复杂度的瓶颈是在算 B,为 \(\mathcal O(n\log^2 n)\)。
P6327 区间加区间 sin 和
tags:基本数据结构
思维题做多了,来点好玩的 DS。
由于 \(\sin (x+v)=\sin x\cos v+\cos x\sin v\),\(\cos(x+v)=\cos x\cos v-\sin x\sin y\),因此维护区间 \(\sin x\) 以及 \(\cos x\) 和就做完了!
P8511 [Ynoi Easy Round 2021] TEST_68
tags: 01Trie,dfs 序
由于 dfs 序的性质,在枚举到一个点时其子树必然未被枚举。因此维护一个全局变量 \(x\) 表示答案,每搜到一个点就加入并在 Trie 上找与其匹配的最大值,更新答案。但这样只能算出 \(j\le i\) 的所有点与其匹配的答案,考虑修正这一答案。
发现一个性质:设全局最大的点对是 \((x,y)\),那么被影响到的只有 \(1\to x\) 以及 \(1\to y\),其余子树答案都为 \(a_x\oplus a_y\)。考虑如何计算 \(1\to x\) 以及 \(1\to y\) 上的答案。
这是很容易实现的,类比我们求只包含 \(j\le i\) 点的方法,将链之外的所有点权插入 Trie,从 \(1\) 到 \(x\) 依次插入点权并查询。这样就可以 \(\mathcal O(n\log n)\) 地做完这道题。
P3592 [POI 2015] MYJ
tags: 区间 DP
设 \(f(l,r,x)\) 为包含于 \([l,r]\) 中的所有顾客,且区间中最小价格不小于 \(x\) 的最大总和,那么枚举间断点 \(k\in(l,r)\),以及权值 \(y\)。这里注意到所有商店的值取顾客的 \(c_i\) 是最优的,因此最多只有 \(4000\) 个不同的值。于是有转移式:
注意到这个是可以从大往小继承的,因此再与 \(f(l,r,x+1)\) 取 \(\max\) 即可。
\(cnt(l,r,x)\) 表示 \([l,r]\) 内 \(c_i\) 值不小于 \(x\) 的顾客个数,注意我们只需计算过 \(k\) 的部分,因此要减掉不过 \(k\) 的答案。
时间复杂度 \(\mathcal O(n^3m)\)。
P5336 [THUSC 2016] 成绩单
tags: 区间 DP
设 \(f(l,r,mn,mx)\) 区间 \([l,r]\) 内,没选的最大值为 \(mx\),最小值为 \(mn\) 的答案。设 \(g(l,r)\) 为 \([l,r]\) 全选完的答案。考虑如何从这个状态推出其他状态。
位置在 \(l\) 上的成绩单可能被取走,或者没有被取走,根据这一点进行分类讨论:
- 若 \(l\) 未被取走,那么 \(f(l,r,\min(mn,a_l),\max(mx,a_l))\gets f(l+1,r,mn,mx)\)
- 若 \(l\) 被取走,那么可以将区间划分为全部被取走的 \([l,k]\) 部分以及剩下没有取走均在 \([k+1,r]\) 的部分。
- 因此,有转移式:\(f(l,r,mn,mx)=\min\limits_{k\in[l,r)}\{g(l,k)+f(k+1,r,mn,mx)\}\)
\(g\) 的转移式也是显然的,即 \(g(l,r)=\min\{f(l,r,mn,mx)+a+b\times(mx-mn)^2\}\)。
对于不合法情况,应该设为 \(+\infty\)。
P6764 [APIO2020] 粉刷墙壁
tags: dp, greedy
题面实在太长/tuu!
一开始读错了题,以为是任意一个请求都会接受,遇到无效情况不刷即可。但发现实际上不是这样。
看一个性质:\(\sum f(x)^2\le 4\times 10^5\)
那么由 Cauthy 不等式有:
因此 \((\sum f(x))^2\le 4\times 10^5\times 10^5\iff \sum f(x)\le 2\times 10^5\),也就是说开发商喜欢颜色个数之和最多也只有 \(2\times 10^5\)。
那么我们的任务就是对于墙壁的每一段 \(i\),是否能向后(包含 \(i\))刷 \(M\) 段。这一步考虑 DP。设 \(f(i,j)\) 为第 \(j\) 个承包商从 \(i\) 开始刷,能刷的最大距离。有转移式:
只要 \(f(i,j)\ge M\),那么就可以从 \(i\) 刷。
接下来是一个贪心,从 \(N-M+1\) 作为起始点,然后一直往最远的合法点跳即可。
为什么不要环形处理?
假设答案存在刷 \(j\in (i-M+1,n]\) 的,那么如果存在可行解,必然前一线段 \(r>i-M+1\)。而每个开发商的后继是固定的,因此必然在 \(k\le i-M+1\) 包含了 \(j\) 的情况。
CF1111D Destroy the Colony
tags: combinatorics, math, dp
题意简述:给出长度为 \(2n\) 的有重复元素的字符串 \(s\),当一种字符都在 \([1,n]\) 或者都在 \([n+1,2n]\) 位置上时称其为合法,你可以对 \(s\) 重排列。\(Q\le 10^5\) 次询问,每次询问给出 \(x,y\),问 \(s_x\) 与 \(s_y\) 的字符在同一侧时,合法排列数。
实际上是对集合的划分。设字符集为 \(U\),字符 \(x\) 出现的次数为 \(c_x\)。那么当其合法,必然存在至少一个集合 \(S\subset U\),使:
考虑一个合法子集 \(S\) 对答案的贡献,也就是存在相同元素的排列问题。先将所有元素视为不同,再固定除 \(x\) 字符不同的元素不动,将 \(x\) 字符重排列,这部分也就是多算的,需要用除法原理去重。即:
那么剩下的字符集 \(T=U-S\),和是 \(n\)。这一部分对答案的贡献是:
二者相互独立,因此总方案为:
所以任意一个合法子集 \(S\subset U\),对答案的贡献都是相同的。于是我们只需用背包统计满足上述条件的集合 \(S\) 个数,再乘上上述的系数和 \(2\)(这是因为可以颠倒),即为没有限制下合法排列总数。
对于 \(Q\) 次询问,实际上有效的仅有 \(52^2\) 个。考虑一对 \(x,y\),我们实际上就是固定这两个数。换句话说,这等价于在删掉 \(x,y\) 的集合 \(U'\) 中,合法排列的个数。枚举每一个 \(x\),强制不选它。设 \(f(j)\) 为容量为 \(j\) 的背包,不选 \(i\) 的方案数。那么对于 \(x=y\) 的答案,即为 \(f(n)\)。对于 \(x\not=y\) 的答案,可以考虑退背包的做法。总的复杂度是 \(\mathcal O(k^2n)\),\(k\) 为字符集的大小。
int up = 1e5;
fac[0] = 1; for (int i = 1; i <= up; i ++) fac[i] = fac[i - 1] * 1ll * i % p;
ifac[up] = qpow (fac[up], p - 2); for (int i = up - 1; ~i; i --) ifac[i] = ifac[i + 1] * 1ll * (i + 1) % p;
scanf ("%s", s + 1); n = strlen (s + 1); n >>= 1; coef = fac[n] * 1ll * fac[n] % p;
for (int i = 1; i <= (n << 1); i ++) cnt[ getNum (s[i]) ] ++;
for (int i = 1; i <= 52; i ++) {
coef = coef * 1ll * ifac[ cnt[i] ] % p;
// if (cnt[i]) fprintf (stderr, "cnt[%d] = %d\n", i, cnt[i]);
}
// fprintf (stderr, "initial = %d\n", coef);
for (int i = 1; i <= 52; i ++) {
if (!cnt[i]) continue;
f[i][0] = 1;
for (int j = 1; j <= 52; j ++) {
if (j == i || !cnt[j]) continue;
for (int k = n; k >= cnt[j]; k --)
add (f[i][k], f[i][k - cnt[j]]);
}
ans[i][i] = f[i][n];
// fprintf (stderr, "Answer without %d = %d\n", i, f[i][n]);
g[0] = 1;
for (int j = i + 1; j <= 52; j ++) {
if (!cnt[j]) continue;
for (int k = 1; k <= n; k ++)
if (k >= cnt[j]) g[k] = minuss (f[i][k], g[k - cnt[j]]);
else g[k] = f[i][k];
ans[j][i] = ans[i][j] = g[n];
// fprintf (stderr, "(%d, %d) : %d\n", i, j, g[n]);
}
}
scanf ("%d", &q);
while (q --) {
int x, y; scanf ("%d%d", &x, &y);
printf ("%d\n", ans[getNum (s[x])][getNum (s[y])] * 1ll * coef * 2 % p);
}