APIO2019 题解
奇怪装置
找最小循环节
模意义下显然是循环节,那么我们需要找到最小循环节,设其为 \(T\),那么 \([0, T - 1]\) 每个数都一一对应着互不相同(因为在同一最小循环节内,显然 \(x, y\) 都是随着 \(t\) 的增加,至少有一个会变化)的 \(T\) 个 \((x, y)\) 点对,然后我们把区间映射到数轴上,就变成了一个区间覆盖问题。
学到了一种找新循环节的方法:即考虑两者相同满足什么充分必要条件,发现这个条件构成一个同余 \(r\),那么最小循环节即 \(r\)。
一开始的思路:
\(x = ((t + \lfloor \frac{t}{B} \rfloor ) \bmod A) = (t \bmod A + \lfloor \frac{t}{B} \rfloor \bmod A) \bmod A\)
显然 ① $t \bmod A $ 、② $ \lfloor \frac{t}{B} \rfloor \bmod A$、③ \(t \bmod B\) 都是具有循环节的,
然后就是找最小循环节。
① 的最小循环节长度是 \(A\)
② 的最小循环节是 $A \times B $
③ 的最小循环节长度是 \(B\)
从这步可以看出总最小循环节必然是 \(A \times B\) 的一个因子。
但还要把 ① 和 ② 加起来然后在模意义下讨论,就不会了...
然后就不要脸的去看了题解 emm
思路也很暴力但我不会,就是讨论 \(t_1\) 与 \(t_2\) 满足对应的 \((x, y)\) 点对相等的充分必要条件是什么。
先带入式子:
不妨设 \(t_1 < t_2\),那么由 ②,可以令 \(t_2 = t_1 + kB\),其中 \(k\) 为任意正整数。
代入 ① 式:
(注意,这里将 \(kB\) 拆出来的原因是 \(\frac{kB}{B}\) 一定是个整数,所以拆出来放进去对取整函数没有影响)
即 \(k(b + 1)\) 为 \(A\) 的倍数。
设正整数 \(c\),\(k(b+1) = cA \Rightarrow k = c \times \frac{A}{B+1}\),将 \(\frac{A}{B+1}\) 化为最简分式,设为 \(\frac{A'}{B'}\),可求 \(A' = \frac{A}{\gcd(A,B+1)}\),那么 \(c\) 必须是 \(B'\) 的倍数,设 \(c = dB'\)(其中 \(d\) 为正整数),那么\(k = dB' \times \frac{A'}{B'} = dA'\)。
所以我们就可以得出结论:当且仅当 \(t_1 \equiv t_2 \pmod {A'B}\) 时,两者对应的点对相同。
所以 \(x\) 的最小循环节长度是 \(A'B\),我们又知道 \(y\) 的最小循环节长度是 \(B\),那么,总最小循环节长度即:
映射线段
那么我们就可以考虑每个线段 \(i\),首先设 \(L = l_i \bmod T, R = r_i \bmod T\),分类讨论将这个线段映射为 \([0, T - 1]\) 的几段:
- 若 \(r_i - l_i + 1 \ge T\),显然整个问题的答案就是 \(T\) 了可以直接退出程序,(或当做 \([0, T - 1]\) 的线段,但没啥意义)
- 否则再分类讨论:
- 若 \(L <= R\),那么映射线段就是 \([L, R]\)
- 否则即对应 \(L > R\) 的情况,映射了两条线段 \([0, R]\) 与 \([L, T - 1]\)
区间覆盖
现在我们的问题变成了,有 \(n\) 数量级的线段,求他们覆盖在数轴上的总长度。
显然贪心即可(区间合并),按左端点排序,即前缀右端点的最大值,加入一条线段,如果能和之前线段接上,就合并,否则在开新的线段,加上之前合并线段的贡献。
\(\text{Tips:}\)
- \(\dfrac{AB}{gcd(A, B + 1)}\) 的计算可能会爆 \(\text{long long}\),那么可以用 \(\text{double}\),如果超过 \(10^{18}\),区间两个端点就不模了就行,因为模不模都一样,不会造成对数的变化。
时间复杂度
\(O(n \log n)\)
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
typedef pair<LL, LL> PLL;
const int N = 1000005;
int n, tot;
LL A, B, l[N], r[N], T;
PLL e[N << 1];
LL gcd(LL a, LL b) {
return b ? gcd(b, a % b) : a;
}
LL solve() {
for (int i = 1; i <= n; i++) {
if (r[i] - l[i] + 1 >= T) return T;
LL L = l[i] % T, R = r[i] % T;
if (L <= R) e[++tot] = make_pair(L, R);
else e[++tot] = make_pair(0, R), e[++tot] = make_pair(L, T - 1);
}
sort(e + 1, e + 1 + tot);
LL ans = 0, L = e[1].first, maxR = e[1].second;
for (int i = 2; i <= tot; i++) {
if (e[i].first <= maxR) maxR = max(maxR, e[i].second);
else ans += maxR - L + 1, L = e[i].first, maxR = e[i].second;
}
return ans + maxR - L + 1;
}
int main() {
scanf("%d%lld%lld", &n, &A, &B);
for (int i = 1; i <= n; i++)
scanf("%lld%lld", l + i, r + i);
LL d = gcd(A, B + 1);
T = ((long double)A * B / d > 1e18 ? 1e18 + 1 : A / d * B);
printf("%lld\n", solve());
return 0;
}
桥梁
- 如果没有修改,那么这就是一道裸的并查集(离线) / 可持久化并查集(在线),即按 \(d\) 排序,依次插入并查集,对应查询就是该点所在连通块的大小,参考 [NOI2018] 归程。但加入修改后,虽然修改时 \(O(1)\),但每次查询是 \(O(m)\) ,总复杂度是 \(O(qm)\)。
所以就想到均摊复杂度,\(50000\) 也是个可以分块的数字。
设查询分为 \(S\) 块,按顺序处理每一块的操作:
- 对于每条边而言,可以分为在此块中修改过(第一类边) / 此块没有修改这条边两种情况(第二类边)。
- 将边按 \(d\) 排序,将询问按 \(w\) 排序。这是 \(O(m\log m)\) 的。
- 按顺序扫描每一个询问,把第二类边的合并贡献加入(老生常谈的双指针)。这是 \(O(m)\) 的
- 考虑第一类边对该询问的影响,暴力将能贡献第一类的边(满足时间轴和重量限制)加入,然后统计答案,然后把这类边撤销(可以用一个可撤销的并查集即可,注意可撤销并查集只能按秩合并做到查询合并 \(O(\log n)\))。这是 \(O(S^2 \log n)\) 的。
综合考虑,总复杂度即 \(O(\dfrac{Q}{S}m \log m + QS \log n)\)。理论取 \(S = \sqrt{\dfrac{m \log m}{\log n}}\) 是最优的。
即总复杂度 \(O(Q\sqrt{m \log m \log n})\),这个还是蛮玄学的QAQ
#include <cstdio>
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 50005, M = 100005;
int n, m, Q, T, ans[M], tot, id[M], d[M];
int f[N], sz[N], top;
struct S {
int u, v;
} st[M];
struct E {
int u, v, d, id;
bool operator<(const E &b) const { return d > b.d; }
} e[M];
struct Qu {
int t, x, y, id;
bool operator<(const Qu &b) const { return y > b.y; }
} q[M], tmp1[M], tmp2[M];
bool vis[M];
int find(int x) { return x == f[x] ? x : find(f[x]); }
void inline merge(int u, int v) {
u = find(u), v = find(v);
if (u == v)
return;
if (sz[u] > sz[v])
swap(u, v);
sz[v] += sz[u], f[u] = v;
st[++top] = (S){ u, v };
}
void inline recall() {
int u = st[top].u, v = st[top].v;
sz[v] -= sz[u], f[u] = u;
--top;
}
void inline work() {
for (int i = 1; i <= m; i++) vis[i] = false;
for (int i = 1; i <= n; i++) f[i] = i, sz[i] = 1;
int t1 = 0, t2 = 0;
top = 0;
for (int i = 1; i <= tot; i++) {
if (q[i].t == 1)
vis[q[i].x] = true, tmp1[++t1] = q[i];
else
tmp2[++t2] = q[i];
}
sort(tmp2 + 1, tmp2 + 1 + t2);
for (int i = 1; i <= m; i++) id[e[i].id] = i;
for (int i = 1, j = 1; i <= t2; i++) {
while (j <= m && e[j].d >= tmp2[i].y) {
if (!vis[e[j].id])
merge(e[j].u, e[j].v);
++j;
}
int lastTop = top;
for (int k = 1; k <= t1; k++) d[tmp1[k].x] = e[id[tmp1[k].x]].d;
for (int k = 1; k <= t1; k++)
if (tmp1[k].id < tmp2[i].id)
d[tmp1[k].x] = tmp1[k].y;
for (int k = 1; k <= t1; k++)
if (d[tmp1[k].x] >= tmp2[i].y)
merge(e[id[tmp1[k].x]].u, e[id[tmp1[k].x]].v);
ans[tmp2[i].id] = sz[find(tmp2[i].x)];
while (top > lastTop) recall();
}
for (int i = 1; i <= t1; i++) e[id[tmp1[i].x]].d = tmp1[i].y;
sort(e + 1, e + 1 + m);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].d), e[i].id = i;
sort(e + 1, e + 1 + m);
T = sqrt(m * log2(m) / log2(n));
scanf("%d", &Q);
for (int i = 1; i <= Q; i++) {
++tot;
scanf("%d%d%d", &q[tot].t, &q[tot].x, &q[tot].y);
q[tot].id = i;
if (tot == T)
work(), tot = 0;
}
if (tot)
work();
for (int i = 1; i <= Q; i++)
if (ans[i])
printf("%d\n", ans[i]);
return 0;
}
路灯
从定义的角度去考虑发现走不通,不妨将每个询问的 \(a, b\) 当做一个二维平面的坐标 \((a, b)\)。
我们可以考虑维护这样一个 \(n \times n\) 的一个二维矩阵,\((a, b)\) 存的是:到当前操作为止来看,\([0, q]\) 总时间内,\(a \Rightarrow b\) 能到达的次数的个数,这样对于每个询问,如果目前仍然可以通行,那么减掉 \(t\) 后面 \(q - t\) 个时刻,否则不能通行,直接输出答案即可。接着我们要考虑修改该如何进行维护(初始化可以一个一个插入,我懒233)。
设 \(L_i, R_i\) 分别表示 \(i\) 点所处的 \(1\) 连续段的最左/右端,这个东西可以用 \(\text{set}\) 维护(考虑将一段\(1\)连续段 \([l, r]\) 插入 \(\text{set}\),以左端点为索引,通过 \(\text{lower_bound}\) 和 \(\text{upper_bound}\) 便能在 \(O(\log n)\) 单点修改 + 维护 \(O(1)\) 维护 \(1\) 连续段与查询 \(L_i, R_i\) 了)
将 0 变成 1
考虑将 \(i\) 点从 \(0\) 变成 \(1\) 的操作,从连续段的变化上考虑,即联通了 \(i, i + 1\)。
故 \([L_i, i]\) 可以作为 \(a\) , \([i+1, R_{i+1}]\) 可以作为 \(b\) ,两者任意选取,这些部分都是新增的可以到达的,所以不考虑后面的操作,设当前时间为 \(t\),这个操作对二维矩阵 \((a,b)\) 处的值影响是 \(+(q-t)\),注意修改操作不会改变当前时刻,只会对之后的时刻造成影响。
即左下角为 \([L_i, i+1]\) ,右上角为 \([i, R_{i+1}]\) 的矩阵,矩阵 \(+(q-t)\)
将 1 变成 0
同理,只不过矩阵 \(-(q-t)\)
数据结构
综上,我们只需维护一个数据结构,支持:
- 矩阵加
- 单点查询
还有时间轴,所以是三维数点,用二维差分把操作改成单点加 + 矩阵查询,这样就能 \(\text{CDQ}\) 了。
时间复杂度
\(O((n + q) \log (n + q) \log n)\)(由于每一个矩阵加差分后要拆成四个单点,所以常数有点大)
#include <cstdio>
#include <iostream>
#include <cstring>
#include <set>
#include <algorithm>
using namespace std;
const int N = 300005;
int n, q, g[N], tot, ans[N], c[N];
char s[N], opt[10];
void inline add(int x, int k) {
for (; x <= n + 1; x += x & -x) c[x] += k;
}
void inline clear(int x) {
for (; x <= n + 1; x += x & -x) c[x] = 0;
}
int inline ask(int x) {
int res = 0;
for (; x; x -= x & -x) res += c[x];
return res;
}
struct P{
int l, r;
bool operator < (const P &b) const {
return l < b.l;
}
};
typedef set<P>::iterator SIT;
set<P> st;
struct Q{
int t, op, x, y, c;
bool operator < (const Q &b) const {
return x < b.x;
}
} e[8 * N];
void cdq(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
cdq(l, mid), cdq(mid + 1, r);
for (int i = mid + 1, j = l; i <= r; i++) {
while (j <= mid && e[j].x <= e[i].x) {
if (e[j].op == 1) add(e[j].y, e[j].c);
++j;
}
if (e[i].op == 2) {
ans[e[i].t] += ask(e[i].y);
}
}
for (int i = l; i <= mid; i++)
if (e[i].op == 1) clear(e[i].y);
sort(e + l, e + r + 1);
}
// 矩阵加差分转化为四个单点加
void inline addToE(int x1, int x2, int y1, int y2, int c, int t) {
e[++tot] = (Q) { t, 1, x1, y1, c };
e[++tot] = (Q) { t, 1, x2 + 1, y1, -c };
e[++tot] = (Q) { t, 1, x1, y2 + 1, -c };
e[++tot] = (Q) { t, 1, x2 + 1, y2 + 1, c };
}
// 将 i -> i+1 这条边 0 变 1
void inline ins(int i, int t) {
g[i] = 1;
// (L_i, i) (i + 1, R_{i + 1})
SIT l = st.upper_bound((P){ i, 0 }); --l;
SIT r = st.lower_bound((P){ i + 1, 0 });
int Li = l->l, Ri = r->r;
addToE(Li, i, i + 1, Ri, q - t, t);
st.erase(l); st.erase(r);
st.insert((P){ Li, Ri });
}
// 将 i -> i+1 这条边 1 变 0
void inline del(int i, int t) {
g[i] = 0;
SIT x = st.upper_bound((P){ i, 0 }); --x;
int L = x->l, R = x->r;
addToE(L, i, i + 1, R, t - q, t);
st.erase(x);
st.insert((P){ L, i }), st.insert((P){ i + 1, R });
}
// 检测 a 当前能否走到 b
bool inline check(int a, int b) {
SIT x = st.upper_bound((P){ a, 0 }); --x;
return b <= x->r;
}
int main(){
memset(ans, -1, sizeof ans);
scanf("%d%d%s", &n, &q, s + 1);
for (int i = 1; i <= n + 1; i++) st.insert((P){ i, i });
for (int i = 1; i <= n; i++)
if (s[i] == '1') ins(i, 0);
for (int t = 1; t <= q; t++) {
scanf("%s", opt);
if (opt[0] == 't') {
int i; scanf("%d", &i);
if (g[i] == 1) del(i, t);
else ins(i, t);
} else {
int a, b; scanf("%d%d", &a, &b);
ans[t] = 0;
if (check(a, b)) ans[t] -= q - t;
e[++tot] = (Q) { t, 2, a, b, 0 };
}
}
cdq(1, tot);
for (int i = 1; i <= q; i++)
if (ans[i] != -1) printf("%d\n", ans[i]);
return 0;
}