Loading

:D 获取中...

Note - 整体二分

什么远古东西。顶一下。

Preface

其实是做题做不动了然后也不想卷 whk 于是跑来写这个。正式完工估计要咕咕咕了。

Introduction

多组询问,对于单组询问可以二分,但是每组暴力二分又会 T,而且又可以离线,修改可以根据 \(mid\) 分到某一边,修改对询问的贡献有结合律、交换律时,可以考虑整体二分。

即定义函数 \(solve(l, r, pt)\) 表示编号在 \(pt\) 集合中的询问答案在 \([l,r]\) 中,然后判断 \(pt\) 中询问的答案与 \(mid\) 的大小关系(这一步往往认为是利用整体二分简化后的问题,可能要用数据结构解决)。然后分成 \(pt1\)\(pt2\) 两个集合递归下去算。当 \(l=r\) 的时候,\(pt\) 集合中的询问答案为 \(l\)。——无名的 PPT

乍看有点抽🐘,可以用带修区间第 \(k\) 小来引入。

首先对于单个询问是很典的 trick,二分答案 \(mid\),设区间中有 \(x\) 个数 \(\leq mid\),讨论一下 \(x\)\(k\) 的关系二分即可。

然后扩展到多个询问,我们根据答案值域二分一个 \(mid\),那么对于每个询问,如果 \(\leq mid\) 的数的个数 \(\geq k\),则这个询问的答案 \(\leq mid\);否则 \(> mid\)。所以对于 \(\leq mid\) 的部分,递归 \(solve(l, mid, pt1)\);对于 \(> mid\) 的部分,递归 \(solve(mid + 1, r, pt2)\),这样我们就把询问也做到了二分。然后 \(l = r\) 的时候所维护的答案区间的答案一定都是 \(l\),这时就可以结束递归了。

维护的是一个单点加区间求和,用树状数组可以做到 \(O(n \log V)\),其中 \(V\) 是值域。则 \(T(k) = 2T(\dfrac{k}{2}) + O(k \log V)\)主定理可以得到 \(T(n) = O(n \log n \log V)\)

修改的本质是删除原来的数和增加修改后的数,其实是两个操作。具体可以看代码。

注意时间复杂度必须和 \(ql, qr\) 相关,而不是 \(n\),否则 \(T(k) = 2T(\dfrac{k}{2}) + O(n \log n)\),退化到奶奶家了(应该是 \(O(2 ^ n n \log n)\))。具体操作就是树状数组加上操作的相反数,绝对不能暴力遍历清零!!!

哦哦还有一点,如果直接用上面的 \(solve(l, r, pt)\),这里 \(pt\) 在 C++ 里相当于一个 vector。但是直接这么写空间会爆炸,更好的实现可以关注代码中的 \(q1, q2\) 数组,基本都可以这么写。

namespace liuzimingc {
const int N = 1e5 + 5;

int n, m, tot, a[N], ans[N], len, bit[N];
struct node {
	int id, l, r, k, type;
} q[N * 3], q1[N * 3], q2[N * 3];

void add(int x, int k) {
	for (; x <= n; x += x & -x) bit[x] += k;
}

int ask(int x) {
	int sum = 0;
	for (; x; x -= x & -x) sum += bit[x];
	return sum;
}

void solve(int l, int r, int ql, int qr) {
	if (ql > qr) return;
	if (l == r) {
		for (int i = ql; i <= qr; i++)
			if (!q[i].type) ans[q[i].id] = l;
		return;
	}
	int mid = l + r >> 1, tot1 = 0, tot2 = 0;
	for (int i = ql; i <= qr; i++)
		if (q[i].type) {
			if (q[i].r <= mid) add(q[i].l, q[i].k), q1[++tot1] = q[i];
			else q2[++tot2] = q[i];
		}
		else {
			int x = ask(q[i].r) - ask(q[i].l - 1);
			if (q[i].k <= x) q1[++tot1] = q[i];
			else q[i].k -= x, q2[++tot2] = q[i];
		} // 这里,分别加入两个数组
	for (int i = ql; i <= qr; i++)
		if (q[i].type)
			if (q[i].r <= mid) add(q[i].l, -q[i].k);
	for (int i = 1; i <= tot1; i++) q[ql + i - 1] = q1[i];
	for (int i = 1; i <= tot2; i++) q[ql + tot1 + i - 1] = q2[i]; // 重新放回 q 中
	solve(l, mid, ql, ql + tot1 - 1);
	solve(mid + 1, r, ql + tot1, qr); // 递归的 [ql, qr] 就是 q 的下标
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> a[i], q[++tot] = (node){ 0, i, a[i], 1, 1 };
	for (int i = 1; i <= m; i++) {
		char op;
		cin >> op;
		if (op == 'Q') {
			tot++;
			cin >> q[tot].l >> q[tot].r >> q[tot].k;
			q[tot].id = ++len;
			q[tot].type = 0;
		}
		else {
			int x, y;
			cin >> x >> y;
			q[++tot] = (node){ 0, x, a[x], -1, 1 };
			q[++tot] = (node){ 0, x, y, 1, 1 };
			a[x] = y;
		}
	}
	solve(1, 1e9, 1, tot);
	for (int i = 1; i <= len; i++)
		cout << ans[i] << endl;
	return 0;
}
} // namespace liuzimingc

Problem

呃呃,因为我觉得游什么老师的 PPT 实在是太好了,所以一些比较简单的题就直接放 PPT 润色吧!!!

流星 Meteors

Link

考虑二分每个国家的时间,每次二分后执行二分位置之前的所有事件,判断是否达到期望。然而这样的时间复杂度太高。

但是我们可以将所有国家一起二分,先执行前一半操作序列试试,如果某个国家已经达到期望就说明该国家的答案 \(\leq mid\),否则说明 \(> mid\)。这样每一轮只用完整地执行一次操作序列,区间加用数据结构实现,时间复杂度 \(O(n\log^2n)\)。这里写的是树状数组维护差分序列。

因为这里有一个很实用的小技巧所以单独说一下,有时候题目要求判无解,除了单独处理外可以把判 \(l = r\) 更新答案的部分放到分完序列之后,这样在当前函数里无解的是不会被更新的。

namespace liuzimingc {
const int N = 3e5 + 5;
#define int unsigned long long

int n, m, o[N], p[N], k, bit[N], to[N], ans[N];
pair<int, int> a[N], a1[N], a2[N];
vector<int> v[N];
struct node {
	int l, r, a;
} q[N];

void add(int x, int k) {
	for (; x <= m; x += x & -x) bit[x] += k;
}

int ask(int x) {
	int sum = 0;
	for (; x; x -= x & -x) sum += bit[x];
	return sum;
}

void update(int l, int r, int k) {
	add(l, k);
	add(r + 1, -k);
}

void solve(int l, int r, int ql, int qr) {
	if (l > r || ql > qr) return;
	// if (l == r) {
	// 	for (int i = ql; i <= qr; i++)
	// 		ans[a[i].first] = l;
	// 	return;
	// }
	int mid = l + r >> 1;
	for (int i = l; i <= mid; i++)
		if (q[i].l <= q[i].r) update(q[i].l, q[i].r, q[i].a);
		else update(q[i].l, m, q[i].a), update(1, q[i].r, q[i].a);
	int tot1 = 0, tot2 = 0;
	for (int i = ql; i <= qr; i++) {
		int sum = 0;
		for (const int &j : v[a[i].first]) sum += ask(j);
		if (sum >= a[i].second) a1[++tot1] = a[i];
		else a[i].second -= sum, a2[++tot2] = a[i];
	}
	for (int i = l; i <= mid; i++)
		if (q[i].l <= q[i].r) update(q[i].l, q[i].r, -q[i].a);
		else update(q[i].l, m, -q[i].a), update(1, q[i].r, -q[i].a);
	if (l == r) {
		for (int i = 1; i <= tot1; i++)
			ans[a1[i].first] = l; // a1 中的元素一定满足条件
		return;
	}
	for (int i = 1; i <= tot1; i++) a[ql + i - 1] = a1[i];
	for (int i = 1; i <= tot2; i++) a[ql + tot1 + i - 1] = a2[i];
	solve(l, mid, ql, ql + tot1 - 1);
	solve(mid + 1, r, ql + tot1, qr);
}

signed main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin >> n >> m;
	for (int i = 1; i <= m; i++) cin >> o[i], v[o[i]].push_back(i);
	for (int i = 1; i <= n; i++) cin >> p[i], a[i] = make_pair(i, p[i]);
	cin >> k;
	for (int i = 1; i <= k; i++)
		cin >> q[i].l >> q[i].r >> q[i].a;
	solve(1, k, 1, n);
	for (int i = 1; i <= n; i++)
		if (ans[i]) cout << ans[i] << endl;
		else cout << "NIE" << endl;
	return 0;
}
#undef int
} // namespace liuzimingc

推荐练习:天天爱射击。据 PPT 有一个小 trick,考虑一个子弹打碎多少木板,可以反过来,考虑一个木板碎的时间,如果在第 \(i\) 时刻碎掉,那么就说明第 \(i\) 发子弹打碎了这个木板,然后就跟上面的差不多了(虽然这个其实很一眼)。

混合果汁

Link

首先可以发现题目要求的就是最小的最大,很明显的二分。

对于单个询问 \(d_j\),直接二分这个最小的美味度 \(d\),然后对于所有 \(d_i \geq d\) 的果汁,按价格从小到大贪心考虑,看最后总价格是否不大于 \(g_j\) 即可。

但是显然过不了,考虑整体二分。这里显然不能每次暴力执行上述操作,抽象一下,其实相当于以价格建立权值线段树,然后在上面询问,优先询问左子树。假设二分出来的是 \(mid\),每次把所有的 \(d_i \geq mid\) 的加入权值线段树后,统一询问。

但是!每次清空再建权值线段树实在是太慢了。我们可以考虑每次只在上一次操作的基础上进行修改。如果 \(mid\) 比之前 \(now\) 大,我们只需要增加 \([now + 1, mid]\) 的部分;如果比之前 \(now\) 小就只需要减少 \([mid + 1, now]\) 的部分。如果美味度是单调的就好了,可以直接指针维护,所以可以直接开始以美味度从大到小排序。然后你就可以用一个类似于莫队的东西优化(这里指针增加相当于美味度变小,本质就是上面的那个):

while (now < mid) now++, update(1, a[now].p, a[now].l);
while (now > mid) update(1, a[now].p, -a[now].l), now--;

然后就有:

namespace liuzimingc {
const int N = 1e5 + 5;
#define int long long
#define endl '\n'

int n, m, ans[N], now;
struct juice {
	int d, p, l;
} a[N];
struct node {
	int g, l, id;
} q[N], q1[N], q2[N];
struct segment_tree {
	int l, r, tot, sum;
} t[N << 2];

void push_up(int p) {
	t[p].tot = t[p << 1].tot + t[p << 1 | 1].tot;
	t[p].sum = t[p << 1].sum + t[p << 1 | 1].sum;
}

void build(int p, int l, int r) {
	t[p].l = l, t[p].r = r;
	t[p].tot = t[p].sum = 0;
	if (l == r) return;
	int mid = l + r >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
}

void update(int p, int x, int tot) {
	if (t[p].l == t[p].r) {
		t[p].tot += tot;
		t[p].sum += tot * x;
		return;
	}
	int mid = t[p].l + t[p].r >> 1;
	if (x <= mid) update(p << 1, x, tot);
	else update(p << 1 | 1, x, tot);
	push_up(p);
}

int ask(int p, int sum) {
	if (t[p].l == t[p].r) return min(t[p].tot, sum / t[p].l);
	if (sum <= t[p << 1].sum) return ask(p << 1, sum);
	else return t[p << 1].tot + ask(p << 1 | 1, sum - t[p << 1].sum);
}

void solve(int l, int r, int ql, int qr) {
	if (ql > qr) return;
	// if (l == r) {
	// 	for (int i = ql; i <= qr; i++)
	// 		ans[q[i].id] = a[l].d;
	// 	return;
	// }
	int mid = l + r >> 1;
	int tot1 = 0, tot2 = 0;
	while (now < mid) now++, update(1, a[now].p, a[now].l);
	while (now > mid) update(1, a[now].p, -a[now].l), now--;
	for (int i = ql; i <= qr; i++) {
		int x = ask(1, q[i].g);
		if (x >= q[i].l) q1[++tot1] = q[i];
		else q2[++tot2] = q[i];
	}
	if (l == r) {
		for (int i = 1; i <= tot1; i++)
			ans[q1[i].id] = a[l].d;
		return;
	}
	for (int i = 1; i <= tot1; i++) q[ql + i - 1] = q1[i];
	for (int i = 1; i <= tot2; i++) q[ql + tot1 + i - 1] = q2[i];
	solve(l, mid, ql, ql + tot1 - 1);
	solve(mid + 1, r, ql + tot1, qr);
}
 
int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> a[i].d >> a[i].p >> a[i].l;
	for (int i = 1; i <= m; i++) cin >> q[i].g >> q[i].l, q[i].id = i;
	sort(a + 1, a + 1 + n, [](juice a, juice b){ return a.d > b.d; });
	build(1, 1, 1e5);
	solve(1, n, 1, m);
	for (int i = 1; i <= m; i++)
		if (ans[i]) cout << ans[i] << endl;
		else cout << -1 << endl;
	return 0;
}
#undef int
} // namespace liuzimingc

Sign on Fence

Link

形式化题意:多次询问 \(l, r, w\),求下面这个玩意的值。

\[\max_{i = l} ^ {r - w + 1} \{ \min_{j = i} ^ {i - w + 1} h_j \} \]

考虑二分的话就是原序列中 \(\geq mid\) 的为 \(1\)\(< mid\) 的为 \(0\),看 \([l, r]\) 最长连续 \(1\) 的个数是否 \(\geq w\)

这个东西可以很典地用线段树维护。类似于上一题,不能每次暴力修改,在之前的基础上 update 就行了。

应 qn 的要求放个代码。

#include <bits/stdc++.h>
 
using namespace std;
 
namespace liuzimingc {
const int N = 1e5 + 5;
 
int n, m, tot, ans[N];
struct node {
	int l, r, k, id;
} q[N << 1], q1[N << 1], q2[N << 1];
struct segment_tree {
	int l, r, lenl, lenr, len, siz;
} t[N << 2];
 
void push_up(int p) {
	t[p].siz = t[p << 1].siz + t[p << 1 | 1].siz;
	t[p].lenl = t[p << 1].len == t[p << 1].siz ? t[p << 1].len + t[p << 1 | 1].lenl : t[p << 1].lenl;
	t[p].lenr = t[p << 1 | 1].len == t[p << 1 | 1].siz ? t[p << 1 | 1].len + t[p << 1].lenr : t[p << 1 | 1].lenr;
	t[p].len = max({ t[p << 1].len, t[p << 1 | 1].len, t[p << 1].lenr + t[p << 1 | 1].lenl });
}
 
void build(int p, int l, int r) {
	t[p].l = l, t[p].r = r;
	if (l == r) {
		t[p].siz = 1;
		return;
	}
	int mid = l + r >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	push_up(p);
}
 
void update(int p, int x, int v) {
	if (t[p].l == t[p].r) {
		t[p].lenl = t[p].lenr = t[p].len = v;
		return;
	}
	int mid = t[p].l + t[p].r >> 1;
	if (x <= mid) update(p << 1, x, v);
	else update(p << 1 | 1, x, v);
	push_up(p);
}
 
segment_tree ask(int p, int l, int r) {
	if (l <= t[p].l && t[p].r <= r) return t[p];
	int mid = t[p].l + t[p].r >> 1;
	if (l > mid) return ask(p << 1 | 1, l, r);
    if (r <= mid) return ask(p << 1, l, r);
    segment_tree a = ask(p << 1, l, r), b = ask(p << 1 | 1, l, r);
    return (segment_tree){ l, r, a.len == a.siz ? a.len + b.lenl : a.lenl, b.len == b.siz ? b.len + a.lenr : b.lenr, max({ a.len, b.len, a.lenr + b.lenl }), a.siz + b.siz };
}
 
void solve(int l, int r, int ql, int qr) {
	if (ql > qr) return;
	if (l == r) {
		for (int i = ql; i <= qr; i++)
			if (q[i].id) ans[q[i].id] = l;
		return;
	}
	int mid = l + r >> 1, tot1 = 0, tot2 = 0;
	for (int i = ql; i <= qr; i++)
		if (!q[i].id) {
			if (q[i].l > mid) update(1, q[i].r, 1), q2[++tot2] = q[i];
			else q1[++tot1] = q[i]; // > mid ? 
		}
	for (int i = ql; i <= qr; i++)
		if (q[i].id) {
			int x = ask(1, q[i].l, q[i].r).len;
			if (x < q[i].k) q1[++tot1] = q[i];
			else q2[++tot2] = q[i];
		}
	for (int i = 1; i <= tot1; i++) q[ql + i - 1] = q1[i];
	for (int i = 1; i <= tot2; i++) q[ql + tot1 + i - 1] = q2[i];
	solve(l, mid, ql, ql + tot1 - 1);
	for (int i = ql + tot1; i <= qr; i++)
		if (!q[i].id)
			if (q[i].l > mid) update(1, q[i].r, 0);
	solve(mid + 1, r, ql + tot1, qr);
}
 
int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		int x;
		cin >> x;
		q[++tot] = (node){ x, i, 0, 0 };
	}
	cin >> m;
	for (int i = 1; i <= m; i++) {
		tot++;
		cin >> q[tot].l >> q[tot].r >> q[tot].k;
		q[tot].id = i;
	}
	build(1, 1, n);
	solve(1, 1e9, 1, tot);
	for (int i = 1; i <= m; i++) cout << ans[i] << " \n";
	return 0;
}
#undef int
} // namespace liuzimingc
 
int main() {
	liuzimingc::main();
	return 0;
}

Sequence 数字序列

知道结论就是傻逼题。自行搜索保序回归。

接水果

考虑 dfs 序,就很典地变成了二维数点问题,对于盘子是覆盖二维区间,水果只需要单点,然后就按 \(x\) 坐标排序扫描线 & 整体二分就好了。

namespace liuzimingc {
const int N = 1e6 + 5;

int n, m1, m2, tot, ans[N], bit[N], dfn[N], siz[N], cnt, f[N][20], dep[N];
vector<int> e[N];
struct query {
	int typ, x, l, r, id, v;
} q[N], q1[N], q2[N];

int lca(int u, int v) {
	if (dep[u] > dep[v]) swap(u, v);
	for (int i = 16; ~i; i--)
		if (dep[f[v][i]] >= dep[u]) v = f[v][i];
	if (u == v) return u;
	for (int i = 16; ~i; i--)
		if (f[u][i] != f[v][i]) u = f[u][i], v = f[v][i];
	return f[u][0];
}

int get(int u, int d) {
	for (int i = 16; ~i; i--)
		if (d >> i & 1) u = f[u][i];
	return u;
}

void dfs(int u, int fa) {
	f[u][0] = fa;
	siz[u] = 1;
	dfn[u] = ++cnt;
	dep[u] = dep[fa] + 1;
	for (const int &v : e[u]) {
		if (v == fa) continue;
		dfs(v, u);
		siz[u] += siz[v];
	}
}

void add(int x, int k) {
	for (; x <= n; x += x & -x) bit[x] += k;
}

int ask(int x) {
	int ans = 0;
	for (; x; x -= x & -x) ans += bit[x];
	return ans;
}

void add(int l, int r, int k) {
	add(l, k);
	if (r + 1 <= n) add(r + 1, -k);
}

void solve(int l, int r, int ql, int qr) {
	if (ql > qr) return;
	if (l == r) {
		for (int i = ql; i <= qr; i++)
			if (!q[i].typ) ans[q[i].id] = l;
		return;
	}
	int mid = l + r >> 1, tot1 = 0, tot2 = 0;
	for (int i = ql; i <= qr; i++)
		if (q[i].typ) {
			if (q[i].v <= mid) add(q[i].l, q[i].r, q[i].id), q1[++tot1] = q[i];
			else q2[++tot2] = q[i];
		}
		else {
			int x = ask(q[i].l);
			if (x >= q[i].v) q1[++tot1] = q[i];
			else q[i].v -= x, q2[++tot2] = q[i];
		}
	for (int i = 1; i <= tot1; i++) q[ql + i - 1] = q1[i];
	for (int i = 1; i <= tot2; i++) q[ql + tot1 + i - 1] = q2[i];
	for (int i = ql; i <= ql + tot1 - 1; i++)
		if (q[i].typ && q[i].v <= mid) add(q[i].l, q[i].r, -q[i].id);
	solve(l, mid, ql, ql + tot1 - 1);
	solve(mid + 1, r, ql + tot1, qr);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin >> n >> m1 >> m2;
	for (int i = 1; i < n; i++) {
		int u, v;
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1, 0);
	for (int j = 1; (1 << j) <= n; j++)
		for (int i = 1; i <= n; i++)
			f[i][j] = f[f[i][j - 1]][j - 1];
	for (int i = 1; i <= m1; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		if (dfn[u] > dfn[v]) swap(u, v);
		int l = lca(u, v);
		if (l == u) {
			int x = get(v, dep[v] - dep[u] - 1);
			q[++tot] = (query){ 1, 1, dfn[v], dfn[v] + siz[v] - 1, 1, w };
			q[++tot] = (query){ 1, dfn[x], dfn[v], dfn[v] + siz[v] - 1, -1, w };
			if (dfn[x] + siz[x] <= n) {
				q[++tot] = (query){ 1, dfn[v], dfn[x] + siz[x], n, 1, w };
				q[++tot] = (query){ 1, dfn[v] + siz[v], dfn[x] + siz[x], n, -1, w };
			}
		}
		else {
			q[++tot] = (query){ 1, dfn[u], dfn[v], dfn[v] + siz[v] - 1, 1, w };
			if (dfn[u] + siz[u] <= n) q[++tot] = (query){ 1, dfn[u] + siz[u], dfn[v], dfn[v] + siz[v] - 1, -1, w };
		}
	}
	for (int i = 1; i <= m2; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		if (dfn[u] > dfn[v]) swap(u, v);
		q[++tot] = (query){ 0, dfn[u], dfn[v], 0, i, w };
	}
	sort(q + 1, q + 1 + tot, [](auto a, auto b){ return a.x != b.x ? a.x < b.x : a.typ > b.typ; });
	solve(0, 1e9, 1, tot);
	for (int i = 1; i <= m2; i++) cout << ans[i] << endl;
	return 0;
}
#undef int
} // namespace liuzimingc

网络

随便做。神秘树剖 + 整体二分 \(\mathcal O(n \log ^ 3 n)\),树上差分 + 整体二分 \(\mathcal O(n \log ^ 2 n)\)

posted @ 2024-08-29 08:19  liuzimingc  阅读(35)  评论(0编辑  收藏  举报