NOIP 模拟赛 8.20
[QOJ1173] Knowledge Is...
给定 \(n\) 个区间,有 \(m\) 个人,每个人可以选择 \(0 \sim 2\) 个区间,要求同一个人选的区间互不相交,一个区间最多被一个人选,求最多能选多少个区间。
\(n, m \le 3 \times 10^5\)
反悔贪心经典模型。考虑把问题转化为最大配对数。按照区间左端点从小到大排序,如果前面有 \(r_j < l_i\) 且尚未配对的 \(j\),则与之配对。但考虑这样直接贪心的问题:考虑一个 \(r_i < r_j\) 很小且 \(l_i < l_j\) 的 \(i\) 已经被配对,首先 \(i\) 一定会先于 \(j\) 遍历到,但是我们发现把较小的 \(r_i\) 给后面的配对机会更大,所以拆散 \(i\) 的配对,转而让 \(j\) 配上之前配的数,这样做就对了。
四向联结
给定一张 \(n \times m\) 的网格图,格子上带权。如果一个格子集合 \(S\) 满足如下条件,我们称它为四向联通的:
- \(S\) 中的格子的权值均为 \(0\);
- \(S\) 中的格子可以通过每次沿上下左右方向移动一格互相到达。
- \(S\) 中包含 \(x=1\), \(x=n\), \(y=1\), \(y=m\) 的点至少各一个。
你需要通过修改若干个格子的权值为 \(0\),使得图中存在四向联通的集合。修改的代价是它原本的权值。
\(n,m \le 10^3\),权值不超过 \(35\)。
草,看错题了。 不难发现它的走法无非就只有 2(或者说 3) 种:
第一种类似于(用 # 表示走过的路):
...#...
...#...
###0###
...#...
...#...
也就是从一个节点出发走四个方向的最短路。这个可以通过 \(4\) 次从边缘出发的多源 Dijkstra 完成。
第二种类似于:
.....#..
....@@##
....0...
....@...
..@0@...
##@.....
..#.....
也就是说选择两个点,每个点先走一段公共路径,再分开走到两个边缘位置。然后这两个点之间跑一个最短路。分开走的这一段是简单的,用刚刚跑出来的结果可以计算出来。
对于两个点之间的最短路我们不可能跑全源最短路,因为这是一个全局的问题,所以考虑建一个超级源点向每个点连一条边,权值为分开走的最短路。同理建一个超级汇点(也可以不用建)。
第三种就是第二种翻转一下。可能我们假定不会重复走的地方实际上重复走了,但是这样显然不优。
复杂度为 \(\mathcal O(nm \log (nm))\),常数大概是 \(6 \times \text{priority\_queue}\) 倍左右。可以用桶优化 Dijkstra 跑的可能会快一点。
[OOI 2026] Tasks from Sasha
给定一棵 \(n\) 个节点的树,给出 \(k\) 个点 \(x_i\)(互异),求长为 \(k\) 的 \(\{S_i\}\) 集合序列方案数满足:
- \(\mathrm{lca}(S_i)=x_i\)。
- \(\forall i\neq j, S_i \cap S_j=\varnothing\)。
- \(S_i \subseteq [n]\),可以不用把 \(1,2,\dots,n\) 选完。
\(3 \le n \le 10^6, k \le \min (n, 2000)\)。
考虑 \(\mathrm{lca}=u\) 是不易刻画的,把它转化为所有点均在 \(u\) 子树内且不存在 \(\mathrm{fa}_v=u\) 满足所有点均在 \(v\) 子树内。容斥一下变成两个相同的限制(在子树中的条件简记为 \(P_u\) 和 \(P_v\),另外还有 \(S_i \neq \varnothing\) 的限制)。首先考虑暴力 DP,定义 \(f_{u, i}\) 表示在 \(u\) 子树内,剩余 \(i\) 个节点没有被用到,在关键点(有 \(x_j=u\))的位置,枚举子树方案即可。复杂度 \(\mathcal O(n^2k)\) 但是问题是在这个解法中,\(n^2\) 状态数难以优化。
考虑一个妙趣横生的转化:把这个问题反过来变成 \(i\) 去匹配祖先节点。定义 \(f_{u, i}\) 表示 \(u\) 节点上面的可匹配的祖先节点有 \(i\) 个的方案数。对于 \(P_u\) 的钦定,\(u\) 显然对于任意在 \(u\) 子树内的节点都是可匹配的祖先,而钦定 \(P_v\) 则只有 \(v\) 子树中的节点可匹配 \(u\)。所以可以简单转移:
- 对于一个关键点,钦定 \(P_u\):\(f_{u, i} \gets (i+2)\displaystyle \prod f_{v, i+1}\)。
- 对于一个关键点,钦定 \(P_v\):\(f_{u, i} \gets \displaystyle-(i+1)\sum f_{v, i+1}\prod _{v'\neq v} f_{v', i}\)。
- 对于一个非关键点:\(f_{u, i} \gets (i+1)\displaystyle\prod f_{v, i}\)。对于关键点转移类似,\(S=\varnothing\) 时,满足 \(P_u\) 会贡献 \(1\),而 \(P_v\) 会贡献 \(-|\mathrm{son}_u|\),系数还得带 \(|\mathrm{son}_u|-1\)。
复杂度降低到 \(\mathcal O(nk)\),还是过不去😮💨。考虑我们的 dp 要保留哪些点,关键点是一定要留的,非关键点的转移则是带上一个幂直接爬到关键点上。然而第二个转移要求我们对每个 \(v\) 都要求一下,而我们肯定不能直接枚举所有 \(v\)(被菊花卡掉)。观察那种一个关键点都没有的 \(v\) 子树,他们的 \(f_{v, i}\) 只与 size 有关。套一个经典的结论,本质不同的 size 最多只有 \(\sqrt {\mathrm{size}_u}\) 个,所以可以对于每个 size 相同的一块求。预处理光速幂,分析一下总复杂度 \(\mathcal O(k^2+\sqrt {nk^3})\)。
[ICPC 2018 Jakarta R] Rotating Gears/[QOJ13154] Rotating Gears
给定一棵 \(n\) 个节点的树,形容一个齿轮阵列,相邻的节点表示一对相邻的齿轮。
正如常识所知:旋转一个齿轮,其所有相邻齿轮都会按照其反方向旋转,而且是连锁的。所有齿轮都是一样的,所以顺时针旋转一个齿轮 \(\alpha\) 度,所有与之相邻的齿轮都会逆时针旋转 \(\alpha\) 度。
有三种操作:
- 从板上取下一个齿轮。
- 放回一个齿轮,保持它被取下时的方向。
- 将一个齿轮顺时针旋转 \(\alpha (0 \le \alpha < 360)\) 度。
每次 \(3\) 操作结束后输出该操作影响的齿轮数量乘 \(\alpha\) 的值,在所有操作结束后,输出所有齿轮顺时针偏离竖直方向的角度 \(\delta_i(0 \le \delta_i < 360)\) 之和。初始所有齿轮都朝上(方向可以假定上面有个箭头)
\(1\le n, q\le 10^5\)
好像比 T3 还简单一些。首先将点染色,可以简单地消除顺逆反转问题;然后考虑这就是每次禁用点和恢复,然后修改操作是对 \(u\) 的一个极大未禁用连通块进行一次整体加的操作。
首先不妨定根,找到这个未禁用连通块的最浅的节点 \(t\),容易发现很容易刻画节点 \(v\) 存在于连通块的条件:
- 在 \(t\) 子树内。
- 设 \(d_i\) 表示到根路径上的禁用点数量,\(d_v=d_t\)。
一次禁用点 \(u\) 的更改相当于对 \(u\) 的整棵子树修改 \(d\)。\(t\) 可以用重链剖分简单求出,此处可以做到单 \(\log\)。因为找到一个禁用点就结束,所以对每个重链用 \(\tt set\) 存下禁用点位置,在跳到最深的禁用点时最多调用\(\mathtt{set.size}\) 和 \(\tt set.begin\) 函数,是 \(\mathcal O(1)\) 的。
一件非常惊奇的事是这时候已经可以用线段树维护了。考虑节点上放的标记 add[i], tag[i] 分别表示 \(d\) 的区间增量和 \(\alpha\) 整体加的总和。维护每个节点区间的 \(\rm lca\),当且仅当儿子的 \(d_\mathrm{lca}\) 与当前节点相同才下传标记 tag[i]。每个节点的 add[i] 都来自于区间的 \(\mathrm{lca}\) 及其祖先节点,先下传了 add[i] 后,tag[i] 的下传独立:由于 add[i] 的操作作用于整个区间,区间内部的 \(d\) 的相等情况不会变,所以说当前节点的 add[i] 对于儿子内部无影响。
再考虑如何维护极大连通块的大小 ,维护一个 cnt[i],当儿子的 \(d_{\mathrm{lca}}\) 与当前节点的相同则上传 cnt[i]。
我们不需要区间维护 \(\mathrm{lca}\),把 add 标记永久化即可。所有操作的复杂度均为单 \(\log\)。
参考实现
#include <bits/stdc++.h>
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define drep(i, r, l) for (int i = r; i >= l; --i)
#define LL long long
#define FASTIO ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define FRE(p) freopen(#p".in", "r", stdin), freopen(#p".out", "w", stdout)
#define eb emplace_back
#define PII pair<int,int>
#define coni const int
using namespace std;
constexpr int N = 2e5+5, M = 5e5+5, RD = 360;
int n, m;
vector<int> G[N];
int fa[N], sz[N], dep[N], son[N], col[N], dfn[N], dfc, nfd[N], tp[N];
void dfs1(int x, int fa) {
sz[x] = 1, ::fa[x] = fa;
for (auto y : G[x]) if (y ^ fa) {
dep[y] = dep[x]+1, col[y] = -col[x], dfs1(y, x);
sz[x] += sz[y];
if (!son[x] || sz[son[x]] < sz[y]) son[x] = y;
}
}
void dfs2(int x, int top) {
dfn[x] = ++dfc, nfd[dfc] = x, tp[x] = top;
if (son[x]) dfs2(son[x], top);
for (auto y : G[x]) if (!dfn[y]) dfs2(y, y);
}
set<int> st[N];
inline int becoder(int x) {
int r = x;
while(x > 0 && (st[tp[x]].empty() || *st[tp[x]].begin() > dfn[x])) r = tp[x], x = fa[tp[x]];
if (!x) return 1;
auto it = st[tp[x]].upper_bound(dfn[x]); --it;
return *it==dfn[x] ? r : nfd[*it+1];
}
struct SGT {
int cnt[N<<2], ad[N<<2], tg[N<<2];
#define lc (p<<1)
#define rc (p<<1|1)
inline void pd(int p) {
if (tg[p]) {
if (!ad[lc]) tg[lc] += tg[p];
if (!ad[rc]) tg[rc] += tg[p];
tg[p] = 0;
}
}
inline void pushup(int p) {
cnt[p] = 0;
if (!ad[lc]) cnt[p] += cnt[lc];
if (!ad[rc]) cnt[p] += cnt[rc];
}
void build(int p=1, int l=1, int r=n) {
cnt[p] = (r-l+1), ad[p] = tg[p] = 0;
if (l == r) return ;
coni mid = (l+r)>>1;
build(lc, l, mid), build(rc, mid+1, r);
}
void modify(int L, int R, int op, int p=1, int l=1, int r=n) {
if (L <= l && r <= R) return ad[p]+=op, void();
coni mid = (l+r)>>1; pd(p);
if (L <= mid) modify(L, R, op, lc, l, mid);
if (mid < R) modify(L, R, op, rc, mid+1, r);
pushup(p);
}
int update(int L, int R, int v, int cur, int p=1, int l=1, int r=n) {
if (cur < 0) return 0;
if (L <= l && r <= R) {
if (cur^ad[p]) return 0;
return tg[p]+=v, cnt[p];
}
coni mid = (l+r)>>1; int re = 0; pd(p);
if (L <= mid) re += update(L, R, v, cur-ad[p], lc, l, mid);
if (mid < R) re += update(L, R, v, cur-ad[p], rc, mid+1, r);
return pushup(p), re;
}
int ask(int x, int p=1, int l=1, int r=n) {
if (l == r) return ad[p];
coni mid = (l+r)>>1; pd(p);
return (x <= mid ? ask(x, lc, l, mid) : ask(x, rc, mid+1, r))+ad[p];
}
int query(int p=1, int l=1, int r=n) {
if (l == r) {
int x = nfd[l];
return (tg[p]%RD*col[x]+RD)%RD;
}
coni mid = (l+r)>>1; pd(p);
return query(lc, l, mid)+query(rc, mid+1, r);
}
} t;
signed main() {
FASTIO;
#ifdef LOCAL
FRE(test);
#endif
cin >> n;
rep(i, 2, n) {
int u, v;
cin >> u >> v;
G[u].eb(v), G[v].eb(u);
}
col[1] = 1, dfs1(1, 0), dfs2(1, 1);
t.build();
cin >> m;
for (int op, x, alp; m--; ) {
cin >> op >> x;
if (op <= 2) {
t.modify(dfn[x], dfn[x]+sz[x]-1, op&1?1:-1);
if (op&1) st[tp[x]].insert(dfn[x]); else st[tp[x]].erase(dfn[x]);
}
else {
cin >> alp; const int u = becoder(x);
cout << t.update(dfn[u], dfn[u]+sz[u]-1, (alp*col[x]+RD)%RD, t.ask(dfn[u]))*alp << '\n';
}
}
cout << t.query() << '\n';
return 0;
}
然而这个做法太聪明了,有没有更 naive 的做法?
考虑每一个禁用点的贡献。对 \(u\) 的极大连通块修改等价于:对 \(t\)(连通块最高顶点)子树整体加,并扣除下面每一个最高禁用点的子树贡献。把 \(t\) 记录下来过后倒着做,每次只需要求到根上贡献和来扣贡献就行了。最后做一次树上差分即可。

浙公网安备 33010602011771号