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)\) 点对相等的充分必要条件是什么。

先带入式子:

\[\begin{cases} t_1 + \lfloor \frac{t_1}{B} \rfloor \equiv t_2 + \lfloor \frac{t_2}{B} \rfloor \pmod A\ ① \\ t_1 \equiv t_2 \pmod B\ ②\end{cases} \]

不妨设 \(t_1 < t_2\),那么由 ②,可以令 \(t_2 = t_1 + kB\),其中 \(k\) 为任意正整数。

代入 ① 式:

\[t_1 + \lfloor \frac{t_1}{B} \rfloor \equiv t_1 + kB + \lfloor \frac{t_1 + kB}{B} \rfloor \pmod A \]

\[t_1 + \lfloor \frac{t_1}{B} \rfloor \equiv t_1 + kB + \lfloor \frac{t_1}{B} \rfloor + k\pmod A \]

(注意,这里将 \(kB\) 拆出来的原因是 \(\frac{kB}{B}\) 一定是个整数,所以拆出来放进去对取整函数没有影响)

\[0 \equiv k(b + 1)\pmod A \]

\(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\),那么,总最小循环节长度即:

\[T = A'B = \dfrac{AB}{gcd(A, B + 1)} \]

映射线段

那么我们就可以考虑每个线段 \(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;
}

posted @ 2020-08-11 21:14  DMoRanSky  阅读(156)  评论(0编辑  收藏  举报