省选同样有很多杂项算法,当中不乏一些非常实用的小技巧。
更新日志
完成重构。
开始重构文章,整理了构成和需要写的部分。
实际上都是必备技能。
log 优化技巧
多种方式可以得到 级别的优化,但是当中有一些别的。
基于二分的优化
二分是对单调性的利用,只要有单调性都可以试试二分!
三分法
现在有一个函数,它是一个单峰或者单谷函数,要求出它的极值。
传统的二分法似乎并不好做,找到 mid 之后,无法确定极值在那一边。我们可以使用三分法:在 内任取两点 ,如果 ,则函数在 中必然单调递增或者单调递减(画个图,然后分类讨论,看 是否分布在极值点两侧,就知道了),反之同理。那么这样就可以分出极值。
模板,代码如下:
查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const double eps = 1e-7;
int n;
double l, r, a[20];
double f(double x) {
double ans = 0, b = 1;
for (int i = n; i >= 0; --i, b *= x)
ans += a[i] * b;
return ans;
}
int main(void) {
scanf("%d%lf%lf", &n, &l, &r);
for (int i = 0; i <= n; ++i)
scanf("%lf", a + i);
while (l + eps < r) {
double lmid = (l + r) / 2, rmid = (mid + r) / 2;
if (f(lmid) > f(rmid)) r = mid;
else l = mid;
}
printf("%.5lf\n", l);
return 0;
}
分数规划
分数规划,一般指 01 分数规划,用来求一个分式的极值,也就求一组 ,最大化或者最小化:
有的时候题目还有一些限制,比如分母至少为 ,恰好有 个 是 。
我们一般使用二分答案来解决这个问题。以最大值为例:
我们使用贪心的方式就可以求出左边的最大值。
模板,要求恰好 个 。那么排序即可。代码如下:
查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const double eps = 1e-6;
int n, k;
double t[100005];
int a[100005], b[100005];
bool check(double x) {
for (int i = 1; i <= n; ++i) t[i] = a[i] - x * b[i];
sort(t + 1, t + n + 1);
double ans = 0;
for (int i = n - k + 1; i <= n; ++i) ans += t[i];
return ans > 0;
}
int main(void) {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
for (int i = 1; i <= n; ++i) scanf("%d", b + i);
double l = 0, r = 1e18;
while (l + eps < r) {
double mid = (l + r) / 2;
if (check(mid)) l = mid;
else r = mid;
}
printf("%.4lf\n", r);
return 0;
}
基于二分单调性的优化
将会在《DP 优化》中出现。
倍增
倍增和二分都需要具有单调性,但是倍增能够解决一些二分不能解决的问题,就像二分出 mid 之后发现 mid 无法简单 check,但是倍增却可以方便的合并信息。
倍增答案
二分答案?倍增答案!二分答案要求答案是具有单调性的,但是倍增同样可以做到!
二分上界不确定的内容的最佳方式是倍增。初始时使用倍增求出上界;求解时,类似于倍增 LCA 的方式,从上界开始,合法的最小值就是不合法的最大值 +1,那么我们从二进制位高到低枚举,如果加上这么多还不满足就一定跳!
// 使用类似于下面的方式求解上界
int st = query(); // 初始边界值
int pw = 0;
while (1) {
if (query(r + (1ll << pw)) != st) break;
r += 1ll << pw; ++pw;
}
// 使用类似于下面的方式求解答案
int ans = 0;
for (int i = 30; i >= 0; --i)
if (!check(ans | (1 << i))) ans |= (1 << i);
ans += 1;
倍增优化 DP
这是倍增的一个重要应用,就是利用 DP 转移状态的单调性来设计一个形如 ,其中 代表长度为 的段。
分治
普及组算法的复仇。
我们知道分治是将复杂的问题拆成多个(一般是两个)相似的子问题,直到最后分成的子问题可以简单求解,然后通过子问题的答案合并出大问题的答案。
我知道你早就知道了上面说的,所以我们还是来看点有意思的吧(在这里没出现的分治均在其它文章里出现)。
普通分治
就是正常分治,我们来看一道题:
求一个平面上最近的点对,点数在 级别。
先将所有点按照 坐标排序,然后开始分治。关键在于如何合并:如果一个点满足 ,其中 代表左右两边答案的最小值,那么我们称点 是合法的。然后将这些合法的点再按照 坐标排序,再进行枚举, 坐标距离大于 就 break
掉。
这样可以保证合并的时间复杂度是 的(需要采用归并排序),具体证明需要通过一些几何的方式,不打算研究。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const i64 INF = 1e15;
int n;
struct Point {
int x, y;
bool operator < (const Point &a) const {
if (x == a.x) return y < a.y;
return x < a.x;
}
} a[400005];
i64 dist(int i, int j) {
i64 x = 1ll * (a[i].x - a[j].x) * (a[i].x - a[j].x);
i64 y = 1ll * (a[i].y - a[j].y) * (a[i].y - a[j].y);
return x + y;
}
bool cmp(const int &x, const int &y) {
return a[x].y < a[y].y;
}
int g[400005];
i64 merge(int l, int r) {
if (l == r) return INF;
if (l + 1 == r) return dist(l, r);
int mid = l + r >> 1;
i64 d1 = merge(l, mid), d2 = merge(mid + 1, r);
i64 d = min(d1, d2);
int tot = 0;
for (int i = l; i <= r; ++i) {
i64 k = abs(a[mid].x - a[i].x);
if (k < INF && k * k < d) g[++tot] = i;
}
sort(g + 1, g + tot + 1, cmp);
for (int i = 1; i < tot; ++i)
for (int j = i + 1; j <= tot && 1ll * (a[g[j]].y - a[g[i]].y) * (a[g[j]].y - a[g[i]].y) < d; ++j)
d = min(d, dist(g[i], g[j]));
return d;
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d%d", &a[i].x, &a[i].y);
sort(a + 1, a + n + 1);
printf("%lld\n", merge(1, n));
return 0;
}
二维分治
其实就是对两个东西进行分治,每次将其中一个东西切半(为了保证效率,一般选择其中区间更长的一个切半),然后合并答案。
给定一个 的 01 矩阵,询问有多少个子矩阵满足只有 个 1。
本题要求恰好有 个 1 的子矩形数量,我们将当前矩形劈成两半(以劈成左一半和右一半为例),那么符合条件的子矩形要么在左半,要么在右半,要么跨越中线。
考虑跨越中线的如何合并。我们枚举子矩形的上下边界,然后开个桶 统计左半矩形所含 数量小于 时左边界的最小值(右半矩形同理),然后直接枚举左半边的 的个数就可以统计了。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
int n, m, K;
i64 ans = 0;
int s[2505][2505], p[10], q[10];
int F(int x1, int y1, int x2, int y2) {
if (x1 > x2 || y1 > y2) return 0;
return s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];
}
void divide(int xl, int yl, int xr, int yr) {
if (xl > xr || yl > yr) return;
if (xl == xr && yl == yr) {
ans += (F(xl, yl, xr, yr) == K);
return;
}
int d = (xr - xl > yr - yl);
if (d) {
int mid = xl + xr >> 1;
divide(xl, yl, mid, yr);
divide(mid + 1, yl, xr, yr);
for (int i = yl; i <= yr; ++i) {
p[0] = mid + 1; q[0] = mid;
for (int k = 1; k <= K + 1; ++k) p[k] = xl, q[k] = xr;
for (int j = i; j <= yr; ++j) {
for (int k = 1; k <= K + 1; ++k) {
while (F(p[k], i, mid, j) >= k) ++p[k];
while (F(mid + 1, i, q[k], j) >= k) --q[k];
}
for (int k = 0; k <= K; ++k)
ans += (p[k] - p[k + 1]) * (q[K - k + 1] - q[K - k]);
}
}
} else {
int mid = yl + yr >> 1;
divide(xl, yl, xr, mid);
divide(xl, mid + 1, xr, yr);
for (int i = xl; i <= xr; ++i) {
p[0] = mid + 1; q[0] = mid;
for (int k = 1; k <= K + 1; ++k) p[k] = yl, q[k] = yr;
for (int j = i; j <= xr; ++j) {
for (int k = 1; k <= K + 1; ++k) {
while (F(i, p[k], j, mid) >= k) ++p[k];
while (F(i, mid + 1, j, q[k]) >= k) --q[k];
}
for (int k = 0; k <= K; ++k)
ans += (p[k] - p[k + 1]) * (q[K - k + 1] - q[K - k]);
}
}
}
}
int main(void) {
scanf("%d%d%d", &n, &m, &K);
for (int i = 1; i <= n; ++i) {
char t[2505]; scanf("%s", t + 1);
for (int j = 1; j <= m; ++j)
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + (t[j] - '0');
}
divide(1, 1, n, m);
printf("%lld\n", ans);
return 0;
}
CDQ 分治
记得归并排序吗?我们要求计算分治中心左右两边部分的信息合并,这部分求值的先后顺序没有要求。这样信息可能可以简单地合并以得到一个分治做法。这种做法用来对时间维度进行降维(就是去掉一个偏序限制),也可以处理带有时间限制的问题(限定合并方向)。这种操作被称为 CDQ 分治。
三维偏序问题。给定 个三维空间上的点,设 表示满足 的 的数量,求满足 的 的数量,要求对所有 给出相应的答案。
解决这类问题的流程如下:
- 找到这个序列的中点 ;
- 将所有点对 划分为三类:
- ,
- ,
- 。
- 拆成左右两半的子序列,然后递归求解;
- 设法处理第二种点对。
也就是说,CDQ 分治就是不断把点对通过递归的方式分给左右两个区间。现在我们来看如何解决三维偏序问题:
分别处理三个信息。第一维可以将原数组按照 排序, 转化为 。注意此时如果数都相同会出问题,因此去个重。
第二维可以在分治时采用类似于归并排序的方式解决(求正序对),不过由于第三维的限制,并不是所有的信息都可以加到答案里的,需要整一个权值树状数组来处理第三维的信息:左半段序列的信息加入树状数组,右半段信息进行统计。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, k;
struct Node {
int a, b, c, cnt, ans;
bool operator< (const Node &a) const {
if (this->a != a.a) return this->a < a.a;
if (b != a.b) return b < a.b;
return c < a.c;
}
} a[100005], T[100005];
int ans[100005];
int C[200005];
void add(int x, int t) { for (; x <= k; x += x & -x) C[x] += t; }
int sum(int x) { int s = 0; for (; x; x -= x & -x) s += C[x]; return s; }
void CDQ(int l, int r) {
if (l == r) return a[l].ans += a[l].cnt - 1, void();
int mid = l + r >> 1; CDQ(l, mid); CDQ(mid + 1, r);
int p = l, q = mid + 1;
for (int i = l; i <= r; ++i) {
if (p <= mid && (q > r || a[p].b <= a[q].b)) add(a[p].c, a[p].cnt), T[i] = a[p++];
else a[q].ans += sum(a[q].c), T[i] = a[q++];
}
for (int i = l; i <= mid; ++i) add(a[i].c, -a[i].cnt);
for (int i = l; i <= r; ++i) a[i] = T[i];
}
int main(void) {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) scanf("%d%d%d", &a[i].a, &a[i].b, &a[i].c), a[i].cnt = 1; sort(a + 1, a + n + 1);
for (int i = 1; i <= n; ++i) if (a[i].a != a[m].a || a[i].b != a[m].b || a[i].c != a[m].c) a[++m] = a[i]; else ++a[m].cnt;
CDQ(1, m);
for (int i = 1; i <= m; ++i) ans[a[i].ans] += a[i].cnt;
for (int i = 0; i < n; ++i) printf("%d\n", ans[i]);
return 0;
}
整体操作上来讲,CDQ 分治的过程与归并排序基本无异。
CDQ 分治在其它地方的应用都是将各类信息转化为偏序关系然后求解,具体可以见 Problemset。
整体二分
如果分治中遵循先递归左子树,再递归右子树的法则,那么维护一个指针去“跟踪”分治中心,这个指针的移动距离是 的。
很多题目都可以使用二分来解决,但是它们只有一组询问。如果有多组询问怎么办?整体二分!又称基于值域的分治算法。
记 为答案的值域, 为答案的定义域(人话:考虑下标在区间 内的操作和询问,询问的答案在 内)。
我们把所有操作按照时间顺序存入数组中,然后开始分治,并利用合适的数据结构(Fenwick 树最为常用)统计当前查询的答案和分治中心 之间的关系,根据查询出来的结果将序列按照分成两份递归处理,最后只有一个点就找到了答案。
你确定这是人话?!嗯,确实不是。
那么我们使用题目来讲人话:
静态区间 小,但是空间限制 10MB。想写主席树和树套树的人洗洗睡吧
我们考虑使用整体二分解决这个题目。先想一想,正常用二分解决全局 小问题怎么做?我们先将数组排序,然后再二分。而多次询问的话,就猜测当前的答案是 ,依次验证每个询问的答案应该是小于等于 还是大于 ,根据此将询问划分成两个部分。注意,如果一个询问的答案是大于 ,那么需要更新它的 :减去在值域 上比它小的数。理由很简单,接下来对右半段询问时不会再考虑之前在左半段比它小的,需要提前把这些减去。
还有一点,不要真的去二分值域,将原数组排序后二分答案的位置。
查看代码
#include <bits/stdc++.h>
#define lowbit(x) (x & -x)
using namespace std;
typedef pair<int, int> pii;
int n, m, C[200005], id[200005];
struct Query {
int l, r, k;
} Q[200005];
struct Number {
int x, id;
bool operator < (const Number &a) const {
return x < a.x;
}
} a[200005];
void add(int x, int k) { for (; x <= n; x += lowbit(x)) C[x] += k; }
int sum(int x) { int res = 0; for (; x; x -= lowbit(x)) res += C[x]; return res; }
int ans[200005], t1[200005], t2[200005], cur[200005];
void solve(int l, int r, int ql, int qr) { // 值域 [l, r],询问区间 [ql, qr]
if (ql > qr) return; // 没有操作,再见
if (l == r) {
for (int i = ql; i <= qr; ++i) ans[id[i]] = a[l].x;
return;
}
int mid = l + r >> 1;
for (int i = l; i <= mid; ++i) add(a[i].id, 1); // 将值域的前一半加上 1
int p = 0, q = 0;
for (int i = ql; i <= qr; ++i) {
int u = id[i];
int s = sum(Q[u].r) - sum(Q[u].l - 1); // 在区间内比小于等于 mid 的数
if (s >= Q[u].k) t1[++p] = u; // 在 [l, mid] 中比它小的数大于其排名,答案应在 [l, mid]
else t2[++q] = u, Q[u].k -= s;
}
int tot = ql - 1;
for (int i = 1; i <= p; ++i) id[++tot] = t1[i];
for (int i = 1; i <= q; ++i) id[++tot] = t2[i];
for (int i = l; i <= mid; ++i) add(a[i].id, -1); // 撤销修改操作
solve(l, mid, ql, ql + p - 1); // 前 p 个询问的答案在 [l, mid]
solve(mid + 1, r, ql + p, qr); // 剩下的在 [mid+1, r]
}
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i].x), a[i].id = i;
sort(a + 1, a + n + 1);
for (int i = 1; i <= m; ++i) scanf("%d%d%d", &Q[i].l, &Q[i].r, &Q[i].k), id[i] = i;
solve(1, n, 1, m);
for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
return 0;
}
整体二分还可以用来解决具有单调性的 DP 转移问题:
[CF868F] Yet Another Minimization Problem.
题目描述:给定一个序列 ,要把它分成 个子段。每个子段的费用是其中相同元素的对数。求所有子段的费用之和的最小值。
,, 。
最暴力的做法是设将序列前 个数划分为 段的最小费用为 ,转移为 。
这个转移是具有单调性的,也就是说如果 ,那么转移过来的地方一定满足 。
证明
假定 ,那么有 。
当两个等号同时成立时,令 一定能取到一个最优解。
否则可以得到 ,由于,这个东西显然是不成立的。
这个 DP 我们可以看作是做 轮。我们可以采用整体二分的思想,求解的询问是当前这一轮 DP 数组的值,每一次分治我们要求解 的答案,找到其转移点作为单调性的分界点。
时间复杂度 。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const i64 INF = 1e12;
int n, k, m;
int a[100005], cnt[100005], L = 1, R;
i64 f[22][100005], sum;
i64 calc(int cl, int cr) {
for (; L > cl; --L) sum += cnt[a[L - 1]], ++cnt[a[L - 1]];
for (; R < cr; ++R) sum += cnt[a[R + 1]], ++cnt[a[R + 1]];
for (; L < cl; ++L) --cnt[a[L]], sum -= cnt[a[L]];
for (; R > cr; --R) --cnt[a[R]], sum -= cnt[a[R]];
return sum;
}
void divide(int ql, int qr, int l, int r) { // 求 [ql, qr] 的 DP 值,最优决策点在 [l, r]
if (l > r) return;
int mid = ql + qr >> 1, pos;
i64 minn = INF;
for (int i = l; i <= min(mid, r); ++i) { // 最大只能到 mid
i64 val = f[k - 1][i - 1] + calc(i, mid);
if (val < minn) minn = val, pos = i;
}
f[k][mid] = minn;
if (ql == qr) return;
divide(ql, mid, l, pos);
divide(mid + 1, qr, pos, r);
}
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
f[0][0] = 0; for (int i = 1; i <= n; ++i) f[0][i] = INF;
for (k = 1; k <= m; ++k) divide(1, n, 1, n);
printf("%lld\n", f[m][n]);
return 0;
}
实际上这一类问题的共同特征是转移点单调,所以维护指针来跟踪分治重心,时间复杂度为 。
线段树分治
其实之前就见过线段树分治:数学计算。我们基于时间用线段树进行操作。基于时间?这不就是 CDQ 分治吗?
实际上线段树分治就是一种维护时间区间的数据结构,而且这玩意是可以在线的(只需要每次都查询一下即可,虽然这样可能很慢)!而且还可以支持撤销操作!这是普通 CDQ 达不到的高度!
维护一个固定体积的 01 背包,支持添加、删除物品和查询答案。
背包添加物品好做删除不好做。建立一棵基于询问时间的线段树,统计每一个物品的有效期并在线段树上区间修改添加物品,最后统一查询计算一遍即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int B = 10000019;
const int M = 1000000007;
int n, k, m, tot = 0, tt = 1, tmp[1005];
struct item {
int l, r, v, w;
} E[15005];
vector<item> T[120005];
void update(int o, int l, int r, int x, int y, int val) {
if (x <= l && r <= y) return T[o].emplace_back(E[val]), void();
int mid = l + r >> 1;
if (x <= mid) update(o << 1, l, mid, x, y, val);
if (mid < y) update(o << 1 | 1, mid + 1, r, x, y, val);
}
void solve(int o, int l, int r, int *f) {
int g[1005]; memcpy(g, f, sizeof(g));
for (item x : T[o])
for (int j = k; j >= x.v; --j)
g[j] = max(g[j], g[j - x.v] + x.w);
if (l == r) {
int res = 0;
for (int i = k; i >= 1; --i) res = (1ll * res * B + g[i]) % M;
return printf("%d\n", res), void();
}
int mid = l + r >> 1;
solve(o << 1, l, mid, g);
solve(o << 1 | 1, mid + 1, r, g);
}
int main(void) {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) {
int v, w; scanf("%d%d", &w, &v);
E[++tot] = {tt, -1, v, w};
}
scanf("%d", &m);
for (int i = 1; i <= m; ++i) {
int op; scanf("%d", &op);
if (op == 1) {
int v, w; scanf("%d%d", &w, &v);
E[++tot] = {tt, -1, v, w};
} else if (op == 2) {
int x; scanf("%d", &x);
E[x].r = tt - 1;
} else ++tt;
} --tt;
for (int i = 1; i <= tot; ++i) {
if (E[i].r == -1) E[i].r = tt;
if (E[i].l <= E[i].r) update(1, 1, tt, E[i].l, E[i].r, i);
}
solve(1, 1, tt, tmp);
return 0;
}
树分治
参考《树形问题进阶》。
经典问题
这里记录了一些经典问题,但是往往遇到时又不知该如何处理。
随机化算法
有的时候不知道怎么做?或者遇到神秘的提交答案题(有些提交答案是不可做优化题)?可以考虑使用随机化。
随机化有两种,一种是操作次数一定,正确性与进行的轮数有关(模拟退火等);另一种是期望操作次数,要求数据随机(除非你的方法很神秘,出题人没想到,但是如果交互库是自适应的就没辙了)。
随机化函数
mt19937 Rnd(time(0));
int rndint(int l, int r) {
return uniform_int_distribution<>(l, r)(Rnd);
}
double rnddb(int l, int r) {
return uniform_real_distribution<>(l, r)(Rnd);
}
爬山法
对于单峰函数,三分可能很难实现,那么可以考虑使用爬山。如果当目前无法直接到达最优解,但是可以判断两个解哪个更优的时候,根据一些反馈信息生成一个新的可能解。
给出 维空间的 个点,求出球心(保证存在)。
对于每一个维度都是单峰函数,因此可以采用爬山法。
假定球心为所有点的重心,然后求出所有点到球心距离的平均值,然后可以计算每个维度上球心距离的改变值(到某个点的距离与平均距离差 差距贡献)。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n; double dis[15]; // 答案到点的距离
double ans[15], cans[15], a[15][15];
void check(void) {
double tot = 0;
for (int i = 1; i <= n + 1; ++i) {
dis[i] = cans[i] = 0;
for (int j = 1; j <= n; ++j) dis[i] += (a[i][j] - ans[j]) * (a[i][j] - ans[j]);
tot += (dis[i] = sqrt(dis[i])) / (n + 1);
}
for (int i = 1; i <= n + 1; ++i)
for (int j = 1; j <= n; ++j)
cans[j] += (dis[i] - tot) / tot * (a[i][j] - ans[j]);
// dis[i] - tot 为当前点与原球心的距离差与平均距离的差,除以 tot 以计算这一维度对平均距离的贡献占比
// a[i][j] - ans[j] 为在当前维度的当前点与原球心距离差,根据此值进行移动
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n + 1; ++i)
for (int j = 1; j <= n; ++j)
scanf("%lf", &a[i][j]), ans[j] += a[i][j];
for (int i = 1; i <= n; ++i) ans[i] /= (n + 1); // 放到重心
for (double T = 20000; T > 1e-4; T *= 0.99996) {
check();
for (int i = 1; i <= n; ++i) ans[i] += cans[i] * T;
}
for (int i = 1; i <= n; ++i) printf("%.3lf ", ans[i]);
return putchar('\n'), 0;
}
模拟退火
给出模拟退火的一般实现方式:新的答案选择要为随机整数乘上当前的温度,然后以 的概率接受当前非最优解(保证 为正,大于 rnd(0, 1)
接受,小于不接受)。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n;
int x[1005], y[1005], w[1005];
mt19937 Rand(20070521);
double energy(double X, double Y) {
double r = 0;
for (int i = 1; i <= n; ++i) r += hypot(X - x[i], Y - y[i]) * w[i];
return r;
}
int rndint(int l, int r) {
return uniform_int_distribution<int>(l, r)(Rand);
}
void sa(double a, double b) {
double ax = a, ay = b, T = 100, ans = energy(ax, ay);
const double delta = 0.99;
while (T > 1e-14) {
double xx = ax + rndint(-INT_MAX, INT_MAX) * T, yy = ay + rndint(-INT_MAX, INT_MAX) * T;
double now = energy(xx, yy), cmp = ans - now;
if (cmp > 0) ans = now, ax = xx, ay = yy;
else if (exp(cmp / T) * INT_MAX > rndint(0, INT_MAX)) ax = xx, ay = yy;
T *= delta;
}
printf("%.3lf %.3lf\n", ax, ay);
}
int main(void) {
scanf("%d", &n); double a = 0, b = 0;
for (int i = 1; i <= n; ++i) scanf("%d%d%d", x + i, y + i, w + i), a += x[i] * w[i] / n, b += y[i] * w[i] / n;
return sa(a, b), 0;
}
实际上对于模拟退火,只有在 较小的时候,取所有情况的最优答案会得到非常棒的答案,但是 足够大时没什么区别,直接将答案也改成不优的也可。
其它随机化
没有统一的方式。有些套路如随机撒点,随机化贪心等,更多的还是要根据具体的问题进行分析。
其它优化技巧
这里放置了一些杂项优化技巧。
简单内容
在《提高优化技巧》出现。
- 贪心:包括排序贪心和反悔贪心。
- 双指针:扫描具有单调性的内容。
- 前缀和与差分:必备技能。
- 单调栈与单调队列:维护单调性的常见手段。
根号分治
根号分治是一种按规模大小分类讨论的思想。对于规模为 的问题,如果我们可以使用 和 的复杂度解决,那么可以在 时使用 算法,否则使用 算法。这样的时间复杂度为 。
B 君对 hash 冲突很感兴趣。他会给出一个正整数序列 。
自然,B 君会把这些数据存进 hash 池。第 会被存进 这个池。这样就能造成很多冲突。
B 君会给定许多个 和 ,询问在模 时, 这个池内 数的总和。
另外,B 君会随时更改 。每次更改立即生效。
对于 的数据,有 .
当 的时候,暴力即可。
当 ,考虑在修改时就更新答案,询问时直接输出。
这样的时间复杂度为 。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, t;
int a[150005];
int res[400][400];
int main(void) {
scanf("%d%d", &n, &m); t = sqrt(n);
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i);
for (int j = 1; j <= t; ++j)
res[j][i % j] += a[i];
}
char s[5]; int x, y;
while (m--) {
scanf("%s%d%d", s, &x, &y);
if (s[0] == 'A') {
if (x > t) {
int ans = 0;
for (int i = y; i <= n; i += x) ans += a[i];
printf("%d\n", ans);
}
else printf("%d\n", res[x][y]);
} else {
for (int i = 1; i <= t; ++i)
res[i][x % i] += y - a[x];
a[x] = y;
}
}
return 0;
}
无向图三元环计数
让度数小的点向度数大的点连边,然后暴力 for 查找。如果一个点的度数大于 ,这样的点不超过 个;如果一个点度数小于 ,那么这样的点最多 个。因此时间复杂度为 。
查看代码
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int n, m, ans, deg[100005];
int u[200005], v[200005], vis[100005];
vector<int> G[100005];
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
scanf("%d%d", u + i, v + i);
++deg[u[i]]; ++deg[v[i]];
}
for (int i = 1; i <= m; ++i) {
int x = u[i], y = v[i];
if (deg[x] > deg[y] || (deg[x] == deg[y] && x > y)) swap(x, y);
G[x].emplace_back(y);
}
for (int u = 1; u <= n; ++u) {
for (int v : G[u]) vis[v] = u;
for (int v : G[u]) for (int w : G[v])
if (vis[w] == u) ++ans;
}
printf("%d\n", ans);
return 0;
}
四元环计数
模板。整体思路跟三元环计数一样,考虑怎样数的不重不漏。枚举一个起点 ,保证它是排名最大的点,然后枚举与它距离为 的点,统计其中无序对 的个数即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, ans, cnt[100005];
int deg[100005], a[100005], rk[100005];
vector<int> G[100005];
int main(void) {
scanf("%d%d", &n, &m);
while (m--) {
int u, v; scanf("%d%d", &u, &v);
G[u].emplace_back(v); G[v].emplace_back(u);
++deg[u]; ++deg[v];
}
for (int i = 1; i <= n; ++i) a[i] = i;
sort(a + 1, a + n + 1, [&](int x, int y) { return deg[x] < deg[y] || (deg[x] == deg[y] && x < y); });
for (int i = 1; i <= n; ++i) rk[a[i]] = i;
for (int u = 1; u <= n; ++u) {
for (int v : G[u]) if (rk[u] > rk[v]) for (int w : G[v]) if (rk[u] > rk[w]) ans += cnt[w]++;
for (int v : G[u]) if (rk[u] > rk[v]) for (int w : G[v]) cnt[w] = 0;
}
return !printf("%d\n", ans);
}
Problemset
嗯,题目真的很杂!
二分与三分
包括二分和三分法以及它们的应用。
[JXOI2017] 加法
二分答案,扫描线维护操作的左端点,右端点贪心地选最大的。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, k, v;
int a[200005];
struct Operation {
int l, r;
bool operator<(const Operation& a) const {
if (l != a.l) return l < a.l;
return r > a.r;
}
} d[200005];
int C[200005];
inline void add(int x, int k) { for (; x <= n; x += x & -x) C[x] += k; }
inline void add(int l, int r, int k) { add(l, k); add(r + 1, -k); }
inline int query(int x) { int r = 0; for (; x; x -= x & -x) r += C[x]; return r; }
bool check(int x) { // 最小值大于等于 x
memset(C, 0, sizeof C);
for (int i = 1; i <= n; ++i) add(i, a[i] - a[i - 1]);
int cnt = 0, p = 0; priority_queue<int> q;
for (int i = 1; i <= n; ++i) {
while (p < m && d[p + 1].l == i) q.push(d[++p].r);
while (!q.empty() && query(i) < x) {
// printf("A %d %d\n", i, q.top());
add(i, q.top(), v), q.pop(), ++cnt;
}
if (query(i) < x) return 0;
}
return cnt <= k;
}
int main(void) {
ios::sync_with_stdio(0);
int T; cin >> T;
while (T--) {
cin >> n >> m >> k >> v; int L = 2e8, R;
for (int i = 1; i <= n; ++i) cin >> a[i], L = min(L, a[i]);
for (int i = 1; i <= m; ++i) cin >> d[i].l >> d[i].r;
sort(d + 1, d + m + 1); R = L + m * v + 1; L -= 1;
while (L + 1 != R) {
int mid = L + R >> 1;
if (check(mid)) L = mid;
else R = mid;
}
cout << L << "\n";
}
return 0;
}
[CF1661F] Teleporters
可以将原问题划分成几段,然后对于每一段放置传送器的话分的约均匀越好,全局的最小两相邻传送机距离应该是一个(尽可能满足平均),这样就可以用 来表示 中额外插入 个的最小代价,显然是好求的。
直接二分需要安装的传送机数量?我们好像没有办法 check
,只知道最多传送机数量的话没有一个合适的贪心策略。我们对另一个条件——总花费进行考虑。因为花费越大直接意味着传送机数量越少。
注意到 随着 的增大单调不增,这样可以在外层二分其值 来代表一个段内的最小传送机距离(类似 wqs 的思想),找出一个 的最大 ,而 越大花费越小,直接利用 来进行贪心求出每一段的最小代价,与 比较来确定二分的答案。
设二分出来的答案是 ,选完之后 的值还有剩余,我们尽可能多的值选择 来榨干 的剩余价值。
时间复杂度 。
查看代码
#include <bits/stdc++.h>
#define i64 long long
#define pii pair<i64, int>
#define fi first
#define se second
using namespace std;
const i64 INF = 1e18 + 500;
int n; i64 m;
int a[200005];
inline i64 f(int x, int k) { // 0->x 放 k
i64 va = (x + k) / (k + 1), vb = x / (k + 1);
int ra = x % (k + 1), rb = k + 1 - ra;
return ra * va * va + rb * vb * vb;
}
int calc(int len, i64 v) { // f(len, x - 1) - f(len, x) >= v 的最小 x
int L = 0, R = len;
while (L + 1 != R) {
int mid = L + R >> 1;
if (f(len, mid - 1) - f(len, mid) >= v) L = mid;
else R = mid;
}
return L;
}
pii check(i64 v) {
pii ans(0, 0);
for (int i = 1; i <= n; ++i) {
int x = calc(a[i], v);
ans.fi += f(a[i], x), ans.se += x;
}
return ans;
}
int main(void) {
scanf("%d", &n); for (int i = 1; i <= n; ++i) scanf("%d", a + i);
for (int i = n; i >= 1; --i) a[i] -= a[i - 1]; scanf("%lld", &m);
i64 L = -1, R = INF + 1;
while (L + 1 != R) {
i64 mid = L + R >> 1;
if (check(mid).fi <= m) L = mid;
else R = mid;
}
pii res = check(L + 1);
printf("%lld\n", res.se + (res.fi - m + L - 1) / L);
return 0;
}
[ZJOI2018] 胖
从 号点到达某一个点后,可以被更新的瞭望塔显然是一段连续的区间,这样我们就可以分别对做右端点进行二分。
设要从 更新,这条路的距离为 ,到达第 个点,那么令 ,在 当中不应该存在距离小于 时距离的点。预处理出图上距离的前缀和 ,距离的最小值要分在 的左右讨论,在 左边时是 ,右边时是 ,询问前 ST 表预处理两个信息即可求出距离的最小值(建立大小为 的 ST 表,询问的时候直接二分出左右端点的位置)。
注意距离相等时更新顺序的问题,二分右端点时要对 的位置做一个单独的讨论。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const i64 INF = 2e18;
int n, m, K;
struct Node {
int p, d;
bool operator< (const Node &a) const {
return p < a.p;
}
} a[200005];
i64 dis[200005];
namespace ST {
int lg[200005]; i64 f[18][200005], g[18][200005];
i64 query(int op, int l, int r) {
l = max(1, l); r = min(r, n);
Node tmp = {l, 0}; l = lower_bound(a + 1, a + K + 1, tmp) - a;
tmp = {r, 0}; r = upper_bound(a + 1, a + K + 1, tmp) - (a + 1);
if (l > r) return INF;
int k = lg[r - l + 1];
if (op == 1) return min(f[k][l], f[k][r - (1 << k) + 1]);
return min(g[k][l], g[k][r - (1 << k) + 1]);
}
void init(void) {
for (int i = 2; i <= K; ++i) lg[i] = lg[i >> 1] + 1;
for (int i = 1; i <= K; ++i)
f[0][i] = a[i].d - dis[a[i].p],
g[0][i] = a[i].d + dis[a[i].p];
for (int i = 1; i <= lg[K]; ++i)
for (int j = 1; j + (1 << i) - 1 <= K; ++j)
f[i][j] = min(f[i - 1][j], f[i - 1][j + (1 << i - 1)]),
g[i][j] = min(g[i - 1][j], g[i - 1][j + (1 << i - 1)]);
}
}
using namespace ST;
bool checkl(int p, int x) { // p 更新到 x,x < p
if (p == x) return 1; int d = abs(p - x);
i64 t1 = query(1, x - d, x) + dis[x];
i64 t2 = query(2, x, x + d - 1) - dis[x];
i64 now = query(2, p, p) - dis[x];
return t1 > now && t2 > now;
}
int calcl(int p) {
int L = 0, R = p + 1;
while (L + 1 != R) {
int mid = L + R >> 1;
if (checkl(p, mid)) R = mid;
else L = mid;
}
return R;
}
bool checkr(int p, int x) { // 从 p 能否更新到 x,x > p
if (p == x) return 1; int d = abs(p - x);
i64 t1 = query(1, x - d + 1, x) + dis[x];
i64 t2 = query(2, x, x + d - 1) - dis[x];
i64 now = query(1, p, p) + dis[x];
if (t1 <= now || t2 <= now) return 0;
if (x + d <= n) return query(2, x + d, x + d) - dis[x] >= now; // p 在 x 左边,相等时会先更新
return 1;
}
int calcr(int p) {
int L = p - 1, R = n + 1;
while (L + 1 != R) {
int mid = L + R >> 1;
if (checkr(p, mid)) L = mid;
else R = mid;
}
return L;
}
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 2; i <= n; ++i) scanf("%lld", dis + i), dis[i] += dis[i - 1];
while (m--) {
scanf("%d", &K);
for (int i = 1; i <= K; ++i) scanf("%d%d", &a[i].p, &a[i].d);
sort(a + 1, a + K + 1); ST::init();
i64 ans = 0;
for (int i = 1; i <= K; ++i) ans += (calcr(a[i].p) - calcl(a[i].p) + 1);
printf("%lld\n", ans);
}
return 0;
}
[USACO18OPEN] Talent Show G
分数规划,但是限制 的和至少为 。
这回不能简单的排序了。但是!我们可以用 01 背包的模型解决这个问题。设 代表体积为 时的最大价值。特别的,当 时,将 强行赋值为 ,转移的时候采用刷表法。这样并不会影响答案,因为是刷表,刷出一个 的状态可以直接存在 里。
查看代码
#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-5;
int n, W;
int a[300], b[300];
double f[1005];
bool check(double x) {
for (int i = 1; i <= W; ++i) f[i] = -1e9;
for (int i = 1; i <= n; ++i)
for (int j = W; j >= 0; --j) {
int k = min(W, j + b[i]);
f[k] = max(f[k], f[j] + a[i] - x * b[i]);
}
return f[W] > 0;
}
int main(void) {
scanf("%d%d", &n, &W);
for (int i = 1; i <= n; ++i) scanf("%d%d", b + i, a + i);
double L = 0, R = 1e9;
while (L + eps < R) {
double mid = (L + R) / 2;
if (check(mid)) L = mid;
else R = mid;
}
printf("%d\n", int(L * 1000));
return 0;
}
倍增
题目都比较简单。
[SCOI2015] 国旗计划
先将战士按照左端点排序,然后将环复制成二倍链。注意到每名边防战士的奔袭区间都不会被其他边防战士的奔袭区间所包含,所以这样排序后右端点就是递增的。设 代表第 个战士能将国旗传递给的最远的战士编号,那么我们就可以依次求解,但是这样单次时间复杂度为 !注意到答案具有单调性(战士越多奔袭距离越长),二分答案似乎并不是很好做(不知道这个战士跳到了哪里),因此考虑倍增!设 代表第 个战士开始借用 (包括自己)的战士的力量可以传递给最远的战士的编号,那么参考倍增 LCA 的方式从高到低开始枚举。同时注意,最后不能跑到 ,只能跑到它前一个,这样再来一个战士就一定能跑过去(因为很可能跑过,所以最后再 +1)。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
int f[22][400005]; // 第 i ~ i+2^j-1 个战士的奔袭位置
int Ans[200005];
struct soldier {
int id, l, r;
bool operator < (const soldier &a) const {
return l < a.l;
}
} a[400005];
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%d%d", &a[i].l, &a[i].r);
if (a[i].r < a[i].l) a[i].r += m;
a[i].id = i;
}
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; ++i) {
a[i + n].id = a[i].id;
a[i + n].l = a[i].l + m;
a[i + n].r = a[i].r + m;
}
for (int i = 1, p = i; i <= n * 2; ++i) {
while (p <= n * 2 && a[p].l <= a[i].r) ++p;
f[0][i] = p - 1;
}
for (int i = 1; i < 20; ++i)
for (int j = 1; j <= n * 2; ++j)
f[i][j] = f[i - 1][f[i - 1][j]];
for (int i = 1; i <= n; ++i) {
int mx = a[i].l + m, ans = 1, p = i; // 初始有 1 个战士
for (int j = 19; j >= 0; --j)
if (f[j][p] && a[f[j][p]].r < mx) ans += 1 << j, p = f[j][p]; // 奔袭到 f[j][p] 的右端点
Ans[a[i].id] = ans + 1;
}
for (int i = 1; i <= n; ++i)
printf("%d ", Ans[i]);
putchar('\n');
return 0;
}
[CF1809F] Traveling in Berland
如果当前位置油价是 ,那么肯定能加满就加满(会用完), 的话能加多少加多少。设 代表 到下一个油价是 的位置的位置, 代表代价,倍增这个过程即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
int n, k;
int a[400005], b[400005], f[19][400005]; // f 为从 j 开始走的下一个 1 的位置
i64 d[400005], s[19][400005]; // s 为从 j 开始走到下一个 1 位置的消耗
i64 calc(int s, int t) {
i64 di = d[t] - d[s - 1];
if (b[s] == 1) return di <= k ? di : di * 2 - k;
return di * 2;
}
int main(void) {
int T; scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &k); int lg = 18;
for (int i = 1; i <= n; ++i) scanf("%d", a + i), a[n + i] = a[i];
for (int i = 1; i <= n; ++i) scanf("%d", b + i), b[n + i] = b[i];
for (int i = 1; i <= n * 2; ++i) d[i] = d[i - 1] + a[i];
int nxt = n * 2 + 1; f[0][nxt] = nxt, s[0][nxt] = 0;
for (int i = n * 2; i >= 1; --i) {
f[0][i] = nxt; s[0][i] = calc(i, nxt - 1);
if (b[i] == 1) nxt = i;
}
for (int i = 1; i <= lg; ++i) for (int j = 1; j <= n * 2 + 1; ++j)
f[i][j] = f[i - 1][f[i - 1][j]], s[i][j] = s[i - 1][j] + s[i - 1][f[i - 1][j]];
for (int i = 1; i <= n; ++i) {
int cur = i; i64 res = 0;
for (int j = lg; j >= 0; --j) if (f[j][cur] <= i + n) {
res += s[j][cur];
cur = f[j][cur];
}
printf("%lld ", res + calc(cur, i + n - 1));
}
putchar('\n');
}
return 0;
}
[???] 最小生成树
给定一张带权无向图,其中边的编号为 。把这些边分组,每组构成原图的一棵生成树,且每棵生成树的边权和都不超过 。同时,她还希望任意两棵生成树都是"不相交"的,即分组后边的编号是连续的。数据范围在 级别。
可以直接贪心+二分答案。但是如果 很小,那么可能会导致二分次数过多。因此可以将二分答案改为倍增答案。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, S;
int fa[100005], u[100005], v[100005], w[100005];
struct edge {
int u, v, w;
bool operator< (const edge &a) const { return w < a.w; }
} e[100005];
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
inline bool Kruskal(int l, int r) {
int ans = 0;
for (int i = 1; i <= n; ++i) fa[i] = i;
for (int i = l; i <= r; ++i) e[i].u = u[i], e[i].v = v[i], e[i].w = w[i];
int tot = 1; sort(e + l, e + r + 1);
for (int i = l; i <= r; ++i) {
int u = find(e[i].u), v = find(e[i].v), w = e[i].w;
if (u == v) continue;
fa[u] = v; ++tot;
if ((ans += w) > S) return 0;
if (tot == n) break;
}
return ans <= S && tot == n;
}
int main(void) {
int T; scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &S); int ans = 0;
for (int i = 1; i <= m; ++i) scanf("%d%d%d", u + i, v + i, w + i);
for (int i = 1, flag = 1; i <= m && flag; ) {
int pw = 0;
while (1) {
if (i + (1 << pw) > m) { ans += Kruskal(i, m); goto over; }
if (!Kruskal(i, i + (1 << pw))) ++pw;
else break;
}
if (!flag) break;
int acc = 1 << pw;
for (int j = pw; j >= 0; --j)
if (Kruskal(i, i + acc - (1 << j))) acc -= 1 << j;
// printf("MST %d %d\n", i, i + acc);
++ans; i += acc + 1;
}
over:
printf("%d\n", ans);
}
return 0;
}
「Wdoi-2」死亡之后愈发愉悦
个可爱数连着 个非可爱数。设 代表 是否为可爱数。求出 的最大 , 的最大 ,则容易根据 解出 。
对于求解 ,考虑倍增。注意倍增时要先跳两个 ,这样保证每一次跳跃的长度不大于以前跳跃的长度,因为区间并不是严格单调的,这样防止跳出区间。
对于 的倍增并不需要从 开始,可以发现 ,因此可以直接先跳一个不超过 的数而不是 。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
map<int, bool> ans;
inline bool query(i64 x) {
if (ans.find(x) != ans.end()) return ans[x];
cout << "? " << x << endl; cin >> ans[x];
return ans[x];
}
inline void answer(i64 x) { cout << "! " << x << endl; }
i64 calc(i64 x, i64 acc) { // 求 j(a + x) ~ j(a + p) 相等的最大 p
bool st = query(x);
if (query(x + 1) != st) return x;
int pw = 0;
while (1ll << pw + 1 <= acc) ++pw;
while (1) {
if (query(x + acc + (1ll << pw)) != st) break;
acc += 1ll << pw; ++pw;
}
for (int i = pw - 1; i >= 0; --i)
if (query(x + acc + (1 << i)) == st) acc += 1ll << i;
return x + acc;
}
int main(void) {
ios::sync_with_stdio(0);
int T; cin >> T;
while (T--) {
ans.clear();
i64 p = calc(0, 1);
i64 q = calc(p + 1, max(1ll, p - 1)) - p;
if (query(0)) answer(q * (q + 1) - p); // a 是可爱数
else answer((q - 1) * (q - 1) - 1 - p); // a 不是可爱数
}
return 0;
}
分治
分治是很实用的思想,这里看几道题来体会一下,因为分治在到处都有渗透。
[UVA1608] Non-boring sequences
一定能在中间找到一个唯一的元素,然后分治求解两边即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[200005], b[200005], p[200005];
int pre[200005], nxt[200005];
bool divide(int l, int r) {
if (l >= r) return true;
int p = l, q = r;
while (p <= q) {
if (pre[q] < l && nxt[q] > r)
if (divide(l, q - 1) && divide(q + 1, r)) return true;
if (pre[p] < l && nxt[p] > r)
if (divide(l, p - 1) && divide(p + 1, r)) return true;
--q; ++p;
}
return false;
}
int main(void) {
int T; scanf("%d", &T); while (T--) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i), b[i] = a[i];
p[i] = 0; pre[i] = 0; nxt[i] = n + 1;
}
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - (b + 1);
for (int i = 1; i <= n; ++i) a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
for (int i = 1; i <= n; ++i) {
if (p[a[i]]) pre[i] = p[a[i]], nxt[p[a[i]]] = i;
p[a[i]] = i;
}
if (divide(1, n)) puts("non-boring");
else puts("boring");
}
return 0;
}
[CF1442D] Sum
注意到数组是单调不降的,因此要取一个数组就会一直取下去直到不能取或者取光了。
所以可以想到一个暴力一点的做法:将一个数组视为一个有体积有价值的物品,然后正反做两遍 01 背包,枚举没取满的那个数组和这个数组取多少个,再枚举前面取的体积,这样就可以得出后面取的体积,并计算出总价值,时间复杂度为 。
这样肯定过不去,发现就是合并太慢了,考虑使用分治算法合并:求解 时,我们先将 加入背包,然后递归求解 ,当 时就可以枚举当前体积了。时间复杂度 。
这个问题被称为缺一背包,意思是其中有一个可以取不满,一般采用上述分治法解决。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
int n, k;
vector<i64> a[3005];
i64 ans = 0, f[3005];
void merge(int l, int r) {
if (l == r) {
for (int i = 0; i <= min(k, (int)a[l].size() - 1); ++i)
ans = max(ans, a[l][i] + f[k - i]);
return;
}
int mid = l + r >> 1;
i64 g[3005];
memcpy(g, f, sizeof(g));
for (int i = mid + 1; i <= r; ++i)
for (int j = k; j >= a[i].size() - 1; --j)
f[j] = max(f[j], f[j - a[i].size() + 1] + a[i][a[i].size() - 1]);
merge(l, mid);
memcpy(f, g, sizeof(f));
for (int i = l; i <= mid; ++i)
for (int j = k; j >= a[i].size() - 1; --j)
f[j] = max(f[j], f[j - a[i].size() + 1] + a[i][a[i].size() - 1]);
merge(mid + 1, r);
}
int main(void) {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) {
int m; scanf("%d", &m); a[i].resize(m + 1);
for (int j = 1, x; j <= m; ++j)
scanf("%lld", &a[i][j]), a[i][j] += a[i][j - 1];
}
merge(1, n);
printf("%lld\n", ans);
return 0;
}
CDQ 分治
维护的是偏序关系。
[CQOI2011] 动态逆序对
给定一个 的排列,按照顺序依次删除 个元素,统计每个元素被删除之前整个序列的逆序对数。
删除不太好做,于是我们把这个过程反过来,改成添加元素,这样每个元素就多了一个“时间”,自身还有“位置”和“大小”,要求 ,这就成了三维偏序问题。
注意在合并的时候,两边的序列位置 都是按顺序排好的,因此要正反做两遍,分别对 和 进行统计。
查看代码
#include <bits/stdc++.h>
#define lowbit(x) (x & -x)
using namespace std;
typedef long long i64;
struct Node {
int t, p, v;
bool operator < (const Node &a) const {
if (t == a.t) return p < a.p;
return t < a.t;
}
} a[100005], T[100005];
int n, m, idx[100005];
i64 ans[100005];
int C[100005];
void add(int x, int k) {
for (; x <= n; x += lowbit(x)) C[x] += k;
}
int sum(int x) {
int res = 0;
for (; x; x -= lowbit(x)) res += C[x];
return res;
}
void CDQ(int l, int r) {
if (l == r) return;
int mid = l + r >> 1;
CDQ(l, mid); CDQ(mid + 1, r);
int p = l, q = mid + 1;
for (int i = l; i <= r; ++i) {
if (p <= mid && (q > r || a[p].p < a[q].p)) add(a[p].v, 1), T[i] = a[p++];
else ans[a[q].t] += sum(n) - sum(a[q].v), T[i] = a[q++];
}
for (int i = l; i <= mid; ++i) add(a[i].v, -1);
p = mid, q = r;
for (int i = l; i <= r; ++i) {
if (p >= l && (q < mid + 1 || a[p].p > a[q].p)) add(a[p].v, 1), --p;
else ans[a[q].t] += sum(a[q].v), --q;
}
for (int i = l; i <= mid; ++i) add(a[i].v, -1);
for (int i = l; i <= r; ++i) a[i] = T[i];
}
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i].v), a[i].p = i, idx[a[i].v] = i, a[i].t = 1;
for (int i = 1, x; i <= m; ++i)
scanf("%d", &x), a[idx[x]].t = m - i + 1;
sort(a + 1, a + n + 1);
CDQ(1, n);
for (int i = 1; i <= m; ++i) ans[i] += ans[i - 1];
for (int i = m; i >= 1; --i) printf("%lld\n", ans[i]);
return 0;
}
[Violet] 天使玩偶
绝对值不好处理,考虑拆开分类讨论,假定 ,只需要找到最大的 即可。离线时间维转化为第三维,CDQ 分治处理四个方向即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1000002;
inline int read(void) {
int x = 0, c = getchar_unlocked();
while (!isdigit(c)) c = getchar_unlocked();
while (isdigit(c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar_unlocked();
return x;
}
int n, m;
struct Node {
int x, y, t; bool f; // f 记录是否为询问
bool operator< (const Node &a) { return x < a.x; }
Node(int x = 0, int y = 0, int t = 0, bool f = 0) : x(x), y(y), t(t), f(f) {}
} a[600005], p[600005], T[600005];
int ans[600005];
int C[1000005];
inline void add(int x, int k) {
for (; x <= N; x += x & -x) C[x] = max(C[x], k);
}
inline int qmax(int x) {
int r = 0;
for (; x; x -= x & -x) r = max(r, C[x]);
return r;
}
inline void clear(int x) {
for (; x <= N; x += x & -x) C[x] = 0;
}
void CDQ(int l, int r) {
if (l == r) return;
int mid = l + r >> 1; CDQ(l, mid); CDQ(mid + 1, r);
// 左端点对右端询问点的影响
for (int p = l, q = mid + 1, tmp; q <= r; ++q) if (a[q].f) {
for (; p <= mid && a[p].x <= a[q].x; ++p) if (!a[p].f) add(a[p].y, a[p].x + a[p].y);
if (tmp = qmax(a[q].y)) ans[a[q].t] = min(ans[a[q].t], a[q].x + a[q].y - tmp); // 注意有可能没有点
}
for (int i = l; i <= mid; ++i) if (!a[i].f) clear(a[i].y);
merge(a + l, a + mid + 1, a + mid + 1, a + r + 1, T + l);
for (int i = l; i <= r; ++i) a[i] = T[i];
}
int main(void) {
n = read(), m = read(); memset(ans, 0x3f, sizeof ans);
for (int i = 1; i <= n; ++i) p[i].x = read() + 1, p[i].y = read() + 1;
for (int i = 1; i <= m; ++i) p[++n].f = read() - 1, p[n].x = read() + 1, p[n].y = read() + 1, p[n].t = i;
for (int i = 1; i <= n; ++i) a[i] = p[i];
CDQ(1, n);
for (int i = 1; i <= n; ++i) a[i] = p[i], a[i].x = N - a[i].x;
CDQ(1, n);
for (int i = 1; i <= n; ++i) a[i] = p[i], a[i].y = N - a[i].y;
CDQ(1, n);
for (int i = 1; i <= n; ++i) a[i] = p[i], a[i].x = N - a[i].x, a[i].y = N - a[i].y;
CDQ(1, n);
for (int i = 1; i <= n; ++i) if (p[i].f) printf("%d\n", ans[p[i].t]);
return 0;
}
[SDOI2011] 拦截导弹
设 代表以 结尾的最长子序列(记为 XLIS)长度,那么 , 代表 XLIS 个数,是三维偏序!然后处理以当前点为起点和终点的答案,然后最终答案就很好统计了。
查看代码
#include <bits/stdc++.h>
using namespace std;
struct Node {
int x, y, z;
friend bool operator< (const Node &a, const Node &b) { return a.x < b.x; }
} a[50005];
bool cmp(Node &a, Node &b) { return a.y < b.y; }
bool cmp2(Node &a, Node &b) { return a.x > b.x; }
int n, h[50005], v[50005];
int mh, mv, b[50005];
int f1[50005], f2[50005];
double g1[50005], g2[50005];
void init(int *a, int &m) {
for (int i = 1; i <= n; ++i) b[i] = a[i]; sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - (b + 1);
for (int i = 1; i <= n; ++i) a[i] = lower_bound(b + 1, b + m + 1, a[i]) - (b + 1);
for (int i = 1; i <= n; ++i) a[i] = m - a[i] + 1;
}
int C[50005]; double F[50005];
void add(int o, int x, double y) {
for (int i = o; i <= n; i += i & -i)
if (C[i] < x) C[i] = x, F[i] = y;
else if (C[i] == x) F[i] += y;
}
auto query(int x) {
int r1 = 0; double r2 = 0;
for (int i = x; i; i -= i & -i)
if (r1 < C[i]) r1 = C[i], r2 = F[i];
else if (r1 == C[i]) r2 += F[i];
return make_pair(r1, r2);
}
void clear(int o) { for (int i = o; i <= n; i += i & -i) C[i] = F[i] = 0; }
void CDQ(int l, int r, int *f, double *g, int type) {
if (l == r) return;
int mid = l + r >> 1; CDQ(l, mid, f, g, type);
sort(a + l, a + mid + 1, cmp); sort(a + mid + 1, a + r + 1, cmp);
for (int p = l, q = mid + 1; q <= r; ++q) {
for (; p <= mid && a[p].y <= a[q].y; ++p) add(a[p].z, f[a[p].x], g[a[p].x]);
auto r = query(a[q].z);
if (r.first + 1 == f[a[q].x]) g[a[q].x] += r.second;
else if (r.first + 1 > f[a[q].x]) f[a[q].x] = r.first + 1, g[a[q].x] = r.second;
}
for (int i = l; i <= mid; ++i) clear(a[i].z);
if (type == 1) sort(a + mid + 1, a + r + 1);
else sort(a + mid + 1, a + r + 1, cmp2);
CDQ(mid + 1, r, f, g, type);
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d%d", h + i, v + i), f1[i] = g1[i] = f2[i] = g2[i] = 1;
init(h, mh); init(v, mv);
for (int i = 1; i <= n; ++i) a[i].x = i, a[i].y = h[i], a[i].z = v[i];
CDQ(1, n, f1, g1, 1);
for (int i = 1; i <= n; ++i) a[i].x = i, a[i].y = mh - h[i] + 2, a[i].z = mv - v[i] + 2;
reverse(a + 1, a + n + 1);
CDQ(1, n, f2, g2, 2);
int ans = 0; double s = 0;
for (int i = 1; i <= n; ++i) ans = max(ans, f1[i]); printf("%d\n", ans);
for (int i = 1; i <= n; ++i) if (f1[i] == ans) s += g1[i];
for (int i = 1; i <= n; ++i)
if (f1[i] + f2[i] - 1 == ans) printf("%.6lf ", g1[i] * g2[i] / s);
else fputs("0 ", stdout);
return putchar('\n'), 0;
}
整体二分
通过跟踪分治中心来得到优秀的做法。
[POI2011] MET-Meteors
有 个成员国。现在它发现了一颗新的星球,这颗星球的轨道被分为 份(第 份和第 份相邻),第 份上有第 个国家的太空站。
这个星球经常会下陨石雨。BIU 已经预测了接下来 场陨石雨的情况。
BIU 的第 个成员国希望能够收集 单位的陨石样本。你的任务是判断对于每个国家,它需要在第几次陨石雨之后,才能收集足够的陨石。
一次询问的话二分答案是可做的,那么我们就考虑整体二分。将环展成二倍链,用一个树状数组维护每个区间的值。注意陨石雨可能落在同一个国家使得答案很大,请使用合理的变量类型。
查看代码
#include <bits/stdc++.h>
#define lowbit(x) (x & -x)
using namespace std;
int n, m, k, ans[300005], id[300005];
int L[300005], R[300005], T[300005];
vector<int> a[300005];
int Q[300005], t1[300005], t2[300005];
__int128 C[600005];
void add(int x, int k) { for (; x <= 2 * m; x += lowbit(x)) C[x] += k; }
__int128 query(int x) { __int128 ans = 0; for (; x; x -= lowbit(x)) ans += C[x]; return ans; }
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[id[i]] = l; return; }
int mid = l + r >> 1, p = 0, q = 0;
for (int i = l; i <= mid; ++i) add(L[i], T[i]), add(R[i] + 1, -T[i]);
for (int i = ql; i <= qr; ++i) {
int u = id[i]; __int128 s = 0;
for (int j : a[u]) s += query(j) + query(j + m);
if (s >= Q[u]) t1[++p] = u;
else t2[++q] = u, Q[u] -= s;
}
int tot = ql - 1;
for (int i = 1; i <= p; ++i) id[++tot] = t1[i];
for (int i = 1; i <= q; ++i) id[++tot] = t2[i];
for (int i = l; i <= mid; ++i) add(L[i], -T[i]), add(R[i] + 1, T[i]);
solve(l, mid, ql, ql + p - 1); solve(mid + 1, r, ql + p, qr);
}
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1, x; i <= m; ++i) scanf("%d", &x), a[x].push_back(i);
for (int i = 1; i <= n; ++i) scanf("%d", &Q[i]), id[i] = i;
scanf("%d", &k);
for (int i = 1; i <= k; ++i) {
scanf("%d%d%d", L + i, R + i, T + i);
if (R[i] < L[i]) R[i] += m;
}
L[k + 1] = 1, R[k + 1] = m, T[k + 1] = 1e9;
solve(1, k + 1, 1, n);
for (int i = 1; i <= n; ++i)
if (ans[i] != k + 1) printf("%d\n", ans[i]); else puts("NIE");
return 0;
}
[CTSC2018] 混合果汁
可以二分出美味度的答案,而又有多组询问,因此考虑整体二分。先加入一个美味度为 ,可以无限买的免费果汁方便处理。将果汁按照美味度从大到小排序。
我们将美味度 的果汁全部加入树状数组。对当前询问的分组需要二分出满足其体积限制的最小价格,只需要考虑比这个价格低的果汁一定要全买,不足的用价格等于这个的果汁补即可。
时间复杂度 ,换成树状数组倍增可以做到 。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
int n, m, id[100005], ans[100005];
int t1[100005], t2[100005];
struct Query {
i64 g, l;
} Q[100005];
struct juice {
int d, p; i64 l; // 美味度,每升价格,最大体积
juice(int d = 0, int p = 0, i64 l = 0) : d(d), p(p), l(l) {}
bool operator < (const juice &a) const {
return d > a.d;
}
} a[100005];
struct Fenwick {
#define lowbit(x) (x & -x)
i64 C[100005];
void add(int x, i64 k) {
++x;
for (; x <= 100000; x += lowbit(x)) C[x] += k;
}
i64 query(int x) {
++x; i64 res = 0;
for (; x; x -= lowbit(x)) res += C[x];
return res;
}
} P, L; // 总价格,总体积
inline void update(int i, i64 flag) {
L.add(a[i].p, flag * a[i].l);
P.add(a[i].p, flag * a[i].l * a[i].p);
}
int find(int x) { // 二分出满足体积限制的最小价格
int l = 0, r = 100001;
while (l + 1 != r) {
int mid = l + r >> 1;
if (L.query(mid) >= Q[x].l) r = mid;
else l = mid;
}
return r;
}
int now;
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[id[i]] = a[l].d;
return;
}
int mid = l + r >> 1, p = 0, q = 0;
while (now < mid) update(++now, 1);
while (now > mid) update(now--, -1);
for (int i = ql; i <= qr; ++i) {
int u = id[i]; int x = find(u);
i64 Pv = P.query(x), Pl = L.query(x);
if (Pl >= Q[u].l && Pv - x * (Pl - Q[u].l) <= Q[u].g) t1[++p] = u;
else t2[++q] = u;
}
int tot = ql - 1;
for (int i = 1; i <= p; ++i) id[++tot] = t1[i];
for (int i = 1; i <= q; ++i) id[++tot] = t2[i];
solve(l, mid, ql, ql + p - 1); solve(mid + 1, r, ql + p, qr);
}
int main(void) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%d%d%d", &a[i].d, &a[i].p, &a[i].l);
a[++n] = juice(-1, 0, 1e18); sort(a + 1, a + n + 1);
for (int i = 1; i <= m; ++i) scanf("%lld%lld", &Q[i].g, &Q[i].l), id[i] = i;
solve(1, n, 1, m);
for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
return 0;
}
整体二分带修 | [Luogu P2617] Dynamic Rankings
带修改的整体二分也很好处理,只需要将修改也作为事件。由于带修数很杂,因此可以选择直接二分值域。这样只需要将 的数加入树状数组统计即可。
查看代码
#include <bits/stdc++.h>
#define lowbit(x) (x & -x)
using namespace std;
int n, m;
int w[100005], ans[300005];
struct operation {
int type, l, r, k;
} Q[300005];
int id[300005], t1[300005], t2[300005];
int C[100005];
void add(int x, int k) { for (; x <= n; x += lowbit(x)) C[x] += k; }
int ask(int x) { int res = 0; for (; x; x -= lowbit(x)) res += C[x]; return res; }
int ask(int l, int r) { return ask(r) - ask(l - 1); }
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[id[i]].type) ans[id[i]] = l;
return;
}
int mid = l + r >> 1, p = 0, q = 0;
for (int i = ql; i <= qr; ++i) {
int u = id[i];
if (Q[u].type) {
if (Q[u].r <= mid) add(Q[u].l, Q[u].k), t1[++p] = u;
else t2[++q] = u;
}
else {
int tmp = ask(Q[u].l, Q[u].r);
if (tmp >= Q[u].k) t1[++p] = u;
else Q[u].k -= tmp, t2[++q] = u;
}
}
int tot = ql - 1;
for (int i = 1; i <= p; ++i) id[++tot] = t1[i];
for (int i = 1; i <= q; ++i) id[++tot] = t2[i];
for (int i = 1; i <= p; ++i) {
int u = t1[i];
if (Q[u].type && Q[u].r <= mid) add(Q[u].l, -Q[u].k);
}
solve(l, mid, ql, ql + p - 1), solve(mid + 1, r, ql + p, qr);
}
int main(void) {
cin >> n >> m; int tot = 0;
for (int i = 1; i <= n; ++i) {
scanf("%d", w + i);
Q[++tot] = {1, i, w[i], 1};
}
for (int i = 1; i <= m; ++i) {
char s[2]; scanf("%s", s);
if (s[0] == 'Q') {
int l, r, k; scanf("%d%d%d", &l, &r, &k);
Q[++tot] = {0, l, r, k};
}
else {
int x, k; scanf("%d%d", &x, &k);
Q[++tot] = {1, x, w[x], -1}; w[x] = k;
Q[++tot] = {1, x, w[x], 1};
}
}
for (int i = 1; i <= tot; ++i) id[i] = i, ans[i] = -1;
solve(0, 1e9, 1, tot);
for (int i = 1; i <= tot; ++i) if (ans[i] != -1) printf("%d\n", ans[i]);
return 0;
}
线段树分治
可以很方便地支持撤销。
[Luogu P5787] 二分图 /【模板】线段树分治
判断二分图可以使用扩展域并查集(拆成黑点和白点),而且需要支持可撤销,因此需要使用启发式合并地并查集。然后线段树分治糊上去即可。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, k, f[200005], siz[200005];
struct edge {
int x, y;
} e[200005];
vector<int> T[400005];
struct Node { int x, y, s; } st[200005]; int tot;
int find(int x) {
if (f[x] == x) return x;
return find(f[x]);
}
void merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) return;
if (siz[x] > siz[y]) swap(x, y);
st[++tot] = {x, y, siz[x]};
f[x] = y; siz[y] += siz[x];
}
void update(int o, int l, int r, int x, int y, int k) {
if (x <= l && r <= y) return T[o].emplace_back(k), void();
int mid = l + r >> 1;
if (x <= mid) update(o << 1, l, mid, x, y, k);
if (mid < y) update(o << 1 | 1, mid + 1, r, x, y, k);
}
void solve(int o, int l, int r) {
bool flag = 1; int lst = tot;
for (int i : T[o]) {
int x = find(e[i].x), y = find(e[i].y);
if (x == y) { // 属于一个集合
for (int j = l; j <= r; ++j) puts("No");
flag = 0; break;
}
merge(e[i].x, e[i].y + n); merge(e[i].y, e[i].x + n);
}
if (flag) {
if (l == r) puts("Yes");
else {
int mid = l + r >> 1;
solve(o << 1, l, mid); solve(o << 1 | 1, mid + 1, r);
}
}
while (tot > lst) {
f[st[tot].x] = st[tot].x; siz[st[tot].y] -= st[tot].s;
--tot;
}
}
int main(void) {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= m; ++i) {
int l, r; scanf("%d%d%d%d", &e[i].x, &e[i].y, &l, &r); ++l;
update(1, 1, k, l, r, i);
}
for (int i = 1; i <= n * 2; ++i) f[i] = i, siz[i] = 1;
solve(1, 1, k);
return 0;
}
[CF576E] Painting Edges
跟上一道题似乎很像,只需要使用 个可撤销并查集维护每一个颜色即可。但是如果答案是 NO 不执行此操作如何处理?
线段树分治的特性是 依次处理,第 次询问的生效区间是 ( 代表下一次修改这条边的时间)。可以在每个叶子上再考虑是否满足二分图的条件(每个询问只会多一条边),然后不满足的话这个修改的颜色改为边当前的颜色。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, k, q, u[500005], v[500005];
int a[500005], c[500005], p[500005], tmp[500005];
int fa[55][1000005], siz[55][1000005], tot;
struct Node { int o, x, y, z; } st[3000005];
inline int find(int o, int x) {
while (x != fa[o][x]) x = fa[o][x];
return x;
}
inline void merge(int o, int x, int y) {
x = find(o, x); y = find(o, y);
if (x == y) return;
if (siz[o][x] > siz[o][y]) swap(x, y);
st[++tot] = {o, x, y, siz[o][x]};
fa[o][x] = y; siz[o][y] += siz[o][x];
}
vector<int> T[2000005];
void update(int o, int l, int r, int x, int y, int k) {
if (x <= l && r <= y) return T[o].emplace_back(k), void();
int mid = l + r >> 1;
if (x <= mid) update(o << 1, l, mid, x, y, k);
if (mid < y) update(o << 1 | 1, mid + 1, r, x, y, k);
}
void solve(int o, int l, int r) {
int lst = tot;
for (int i : T[o]) if (c[i]) merge(c[i], u[a[i]], v[a[i]] + n), merge(c[i], v[a[i]], u[a[i]] + n);
if (l == r) {
if (find(c[l], u[a[l]]) == find(c[l], v[a[l]])) // 检查这次操作是否合法
puts("NO"), c[l] = tmp[a[l]]; // 不合法,阻止接下来的合并
else
puts("YES"), tmp[a[l]] = c[l];
} else {
int mid = l + r >> 1;
solve(o << 1, l, mid); solve(o << 1 | 1, mid + 1, r);
}
for (; tot > lst; --tot) {
int x = st[tot].x, o = st[tot].o;
fa[o][x] = x, siz[o][st[tot].y] -= st[tot].z;
}
}
int main(void) {
scanf("%d%d%d%d", &n, &m, &k, &q);
for (int i = 1; i <= k; ++i) for (int j = 1; j <= n * 2; ++j) fa[i][j] = j, siz[i][j] = 1;
for (int i = 1; i <= m; ++i) scanf("%d%d", u + i, v + i), p[i] = q + 1;
for (int i = 1; i <= q; ++i) scanf("%d%d", a + i, c + i);
for (int i = q; i >= 1; --i) {
if (i < p[a[i]] - 1) update(1, 1, q, i + 1, p[a[i]] - 1, i); // 在此次操作之后再生效
p[a[i]] = i;
}
return solve(1, 1, q), 0;
}
[CF603E] Pastoral Oddities
满足题目条件意味着所有连通块的大小都是偶数。将边按照权值从小到大排序,然后线段树分治从右到左依次处理每一条边,计算每一条边可以被记入答案的范围。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, ans[300005];
struct edge {
int u, v, w, id;
bool operator< (const edge &a) const { return w < a.w; }
} e[300005];
int fa[300005], siz[300005];
struct Node {
int x, y, s, t;
} st[300005];
int tot, odd;
int find(int x) { return fa[x] == x ? x : find(fa[x]); }
void merge(int x, int y) {
x = find(x); y = find(y);
if (x == y) return;
if (siz[x] > siz[y]) swap(x, y);
st[++tot] = {x, y, siz[x]};
if (siz[x] % 2 && siz[y] % 2) odd -= 2, st[tot].t += 2;
fa[x] = y; siz[y] += siz[x];
}
vector<int> T[1200005];
void update(int o, int l, int r, int x, int y, int k) {
if (x <= l && r <= y) return T[o].push_back(k), void();
int mid = l + r >> 1;
if (x <= mid) update(o << 1, l, mid, x, y, k);
if (mid < y) update(o << 1 | 1, mid + 1, r, x, y, k);
}
int pos;
void solve(int o, int l, int r) {
int lst = tot;
for (int i : T[o]) merge(e[i].u, e[i].v);
if (l == r) {
while (1) {
if (odd == 0 || pos == m) break;
if (e[pos + 1].id <= l) {
merge(e[pos + 1].u, e[pos + 1].v);
if (e[pos + 1].id < l)
update(1, 1, m, e[pos + 1].id, l - 1, pos + 1);
}
++pos;
}
ans[l] = (odd ? -1 : e[pos].w);
}
else {
int mid = l + r >> 1;
solve(o << 1 | 1, mid + 1, r); solve(o << 1, l, mid);
}
for (; tot > lst; --tot) {
int x = st[tot].x, y = st[tot].y, s = st[tot].s, t = st[tot].t;
fa[x] = x, siz[y] -= s, odd += t;
}
}
int main(void) {
scanf("%d%d", &n, &m); odd = n;
for (int i = 1; i <= n; ++i) fa[i] = i, siz[i] = 1;
for (int i = 1; i <= m; ++i) scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w), e[i].id = i;
sort(e + 1, e + m + 1); solve(1, 1, m);
for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
return 0;
}
随机化算法
比较有趣。
[CF1305F] Kuroni and the Punishment
发现答案至多为 ,因此方案中不变、 的数至少有一半,随机钦定这些数,答案必定是它们的质因数的倍数。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
int n;
i64 a[200005], ans = 1e18;
mt19937 Rand(time(0));
void getAns(i64 x) { // 全部变为 x 的倍数
i64 res = 0;
for (int i = 1; i <= n; ++i) {
i64 t = x - a[i] % x;
if (a[i] >= x) t = min(t, a[i] % x);
res += t;
}
ans = min(ans, res);
}
void work(i64 x) { // x 的全部质因数可能成为答案
for (i64 i = 2; i * i <= x; ++i) if (x % i == 0) {
getAns(i);
while (x % i == 0) x /= i;
}
if (x > 1) getAns(x);
}
int main(void) {
ios::sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
shuffle(a + 1, a + n + 1, Rand);
for (int i = 1; i <= min(n, 50); ++i) {
if (a[i] > 1) work(a[i] - 1);
work(a[i]); work(a[i] + 1);
}
return cout << ans << "\n", 0;
}
[THUSCH2017] 巧克力
如果颜色数比较少的话直接用斯坦纳树做,但是颜色数很多,钦定的可能也很多。
这种 NPC 问题可以直接考虑乱搞, 很小,因此考虑将所有颜色随机映射到 ,然后求最小斯坦纳树即可求出最小的巧克力个数 。这 个点被分配到不同的颜色时答案合法,正确概率是 。随机化做 次即可。
然后二分出中位数,将小于等于二分值的权值都设为 ,大于的都设为 ,然后最小斯坦纳树要 ( 设置为一个不会影响斯坦纳树选择的巧克力数的一个数即可)。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int DX[] = {-1, 1, 0, 0}, DY[] = {0, 0, 1, -1};
int n, m, k, tot;
int c[240][240], a[240][240], w[240][240];
int cc[240], f[240][240][32], to[240];
bool inq[240][240];
mt19937 Rand(time(0));
queue<pair<int, int>> q;
void SPFA(int s) {
while (!q.empty()) {
auto u = q.front(); q.pop(); int x = u.first, y = u.second; inq[x][y] = 0;
for (int i = 0; i < 4; ++i) {
int tx = x + DX[i], ty = y + DY[i];
if (tx < 1 || tx > n || ty < 1 || ty > m || c[tx][ty] == -1) continue;
if (f[tx][ty][s] > f[x][y][s] + w[tx][ty]) {
f[tx][ty][s] = f[x][y][s] + w[tx][ty];
if (!inq[tx][ty]) q.emplace(tx, ty), inq[tx][ty] = 1;
}
}
}
}
int work(void) { // 选择 k 个点的最小代价
int ans = INF;
for (int opt = 1; opt <= 200; ++opt) {
shuffle(cc + 1, cc + tot + 1, Rand);
for (int i = 1; i <= tot; ++i) to[cc[i]] = (i - 1) % k;
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) {
for (int s = 1; s < 1 << k; ++s) f[i][j][s] = INF;
if (c[i][j] != -1) f[i][j][1 << to[c[i][j]]] = w[i][j];
}
for (int s = 1; s < 1 << k; ++s) {
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) if (c[i][j] != -1) {
for (int t = s - 1 & s; t; t = t - 1 & s)
f[i][j][s] = min(f[i][j][s], f[i][j][t] + f[i][j][s ^ t] - w[i][j]);
if (f[i][j][s] < INF) q.emplace(i, j), inq[i][j] = 1;
} SPFA(s);
}
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) ans = min(ans, f[i][j][(1 << k) - 1]);
}
return ans;
}
void solve(void) {
scanf("%d%d%d", &n, &m, &k); tot = 0;
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) {
scanf("%d", &c[i][j]);
if (c[i][j] != -1) cc[++tot] = c[i][j];
}
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j)
scanf("%d", &a[i][j]), w[i][j] = 1;
sort(cc + 1, cc + tot + 1); tot = unique(cc + 1, cc + tot + 1) - (cc + 1);
int rec = work(); if (rec == INF) return puts("-1 -1"), void();
int L = 0, R = 1000001;
while (L + 1 != R) {
int mid = L + R >> 1;
for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) w[i][j] = (a[i][j] <= mid ? 54289 : 54291);
if (work() <= rec * 54290) R = mid; else L = mid;
}
printf("%d %d\n", rec, R);
}
int main(void) {
int T; scanf("%d", &T);
while (T--) solve();
return 0;
}
根号分治
就是针对根号的分类讨论,或者针对两种数据范围给出不同的解法。
[CF797E] Array Queries
当 时暴力,否则预处理出答案。时间复杂度 。
查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const int BLOCK_SIZE = 200;
int n, m;
int a[100005], f[205][100005];
int main(void) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
for (int k = 1; k < BLOCK_SIZE; ++k)
for (int p = n; p >= 1; --p)
f[k][p] = (p + a[p] + k > n ? 1 : f[k][p + a[p] + k] + 1);
scanf("%d", &m); while (m--) {
int p, k; scanf("%d%d", &p, &k);
if (k >= BLOCK_SIZE) {
int res = 0;
while (p <= n) p += a[p] + k, ++res;
printf("%d\n", res);
} else printf("%d\n", f[k][p]);
}
return 0;
}
[CF710F] String Set Queries
不同长度的字符串最多只有 个,对每一个开一个 multiset
统计出现次数。
查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
const i64 B = 10079, P = 110765311;
const int N = 300000;
int n, id[300005], tot, rev[300005];
char s[300005];
i64 b[300005], h[300005];
unordered_multiset<i64> H[805];
int main(void) {
int m, op; scanf("%d", &m);
for (int i = b[0] = 1; i <= N; ++i) b[i] = b[i - 1] * B % P;
while (m--) {
scanf("%d%s", &op, s + 1); n = strlen(s + 1);
for (int i = 1; i <= n; ++i) h[i] = (h[i - 1] * B + s[i]) % P;
if (op == 1) {
if (!id[n]) rev[id[n] = ++tot] = n;
H[id[n]].insert(h[n]);
} else if (op == 2) H[id[n]].erase(h[n]);
else {
int ans = 0;
for (int i = 1, L; i <= tot; ++i)
for (int j = L = rev[i]; j <= n; ++j)
ans += H[i].count((h[j] - h[j - L] * b[L] % P + P) % P);
printf("%d\n", ans); fflush(stdout);
}
}
return 0;
}
[CF1446D2] Frequency Problem (Hard Version)
首先一个结论:其中一定有一个众数是全局众数。否则一定可以扩展这个子段使得全局众数变为其中之一。
采用根号分治,如果一个数的出现次数大于 ,那么可以直接扫描序列,记录一个 代表全局众数和当前选择数的差值,记录可以取到这个差值的位置最小值就是候选答案子段的左侧。
否则,枚举出现次数 ,然后用双指针扫描序列, 往右走, 是满足所有数的出现次数都 的最左端,如果出现次数为 的数的个数至少为 ,那么当前子段可以成为答案。
时间复杂度 。
查看代码
#include <bits/stdc++.h>
using namespace std;
int n, t;
int a[200005], cnt[200005];
int tmp[400005];
int main(void) {
scanf("%d", &n); t = sqrt(n); int ans = 0, mode = 0;
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i), ++cnt[a[i]];
if (cnt[a[i]] > cnt[mode]) mode = a[i];
}
for (int x = 1; x <= n; ++x) if (cnt[x] > t) { // 出现次数大于 sqrt n
if (x == mode) continue;
int res = 0; memset(tmp, 0, sizeof tmp);
for (int i = 1; i <= n; ++i) {
if (a[i] == mode) ++res;
else if (a[i] == x) --res;
if (tmp[res + n] || res == 0) ans = max(ans, i - tmp[res + n]);
else tmp[res + n] = i;
}
}
for (int x = 1; x <= t; ++x) { // 枚举出现次数 x
int L = 1, res = 0;
memset(tmp, 0, sizeof tmp); // i 的出现次数
for (int R = 1; R <= n; ++R) {
if (++tmp[a[R]] == x) ++res;
for (; L <= R && tmp[a[R]] > x; ++L) if (tmp[a[L]]-- == x) --res;
if (res >= 2) ans = max(ans, R - L + 1);
}
}
printf("%d\n", ans); return 0;
}
[Ynoi2011] 初始化
将原序列按照 分块,对于 的时候可以直接暴力修改,否则对于不同的 可以按照 分块,对于一个块内修改前后缀和(单点修改区间查询转化成区间修改单点查询)。
时间复杂度 ,但是块长可以调小,因为暴力常数很小。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int P = 1000000007;
const int BLOCK_SIZE = 110;
inline void add(int &x, int t) { x += t; if (x >= P) x -= P; }
inline void del(int &x, int t) { x -= t; if (x < 0) x += P; }
int n, m, a[200005], sum[3005];
int pos[200005], L[3005], R[3005];
int pre[155][155], suf[155][155];
int query(int l, int r) {
int p = pos[l], q = pos[r], res = 0;
if (p == q) for (int i = l; i <= r; ++i) add(res, a[i]);
else {
for (int i = L[q]; i <= r; ++i) add(res, a[i]);
for (int i = R[p]; i >= l; --i) add(res, a[i]);
for (int i = p + 1; i < q; ++i) add(res, sum[i]);
} return res;
}
int main(void) {
scanf("%d%d", &n, &m); int t = n / BLOCK_SIZE;
for (int i = 1; i <= n; ++i) scanf("%d", a + i);
for (int i = 1; i <= t; ++i) L[i] = R[i - 1] + 1, R[i] = i * BLOCK_SIZE;
if (R[t] < n) { ++t; L[t] = R[t - 1] + 1; R[t] = n; }
for (int i = 1; i <= t; ++i) for (int j = L[i]; j <= R[i]; ++j) pos[j] = i, add(sum[i], a[j]);
while (m--) {
int op; scanf("%d", &op);
if (op == 1) {
int x, y, z; scanf("%d%d%d", &x, &y, &z);
if (x >= BLOCK_SIZE) for (int i = y; i <= n; i += x) add(a[i], z), add(sum[pos[i]], z);
else {
for (int i = 1; i <= y; ++i) add(suf[x][i], z);
for (int i = y; i <= x; ++i) add(pre[x][i], z);
}
} else {
int l, r; scanf("%d%d", &l, &r);
int ans = query(l, r);
for (int i = 1; i < BLOCK_SIZE; ++i) {
int p = (l - 1) / i + 1, q = (r - 1) / i + 1;
if (p == q) {
del(ans, pre[i][(l - 1) % i]);
add(ans, pre[i][(r - 1) % i + 1]);
} else {
add(ans, 1ll * (q - p - 1) * pre[i][i] % P);
add(ans, pre[i][(r - 1) % i + 1]);
add(ans, suf[i][(l - 1) % i + 1]);
}
}
printf("%d\n", (ans % P + P) % P);
}
}
return 0;
}
[CF1039D] You Are Given a Tree
如果 给定,那么考虑 树形 DP(因为只要能选一定不劣)就可解决。
发现答案的取值比较少。考虑根号分治,对于小于等于阈值的部分可以直接 树形 DP 解决(需要卡常,考虑转到 DFS 序上 DP)。
大于阈值的部分发现答案只有 种,考虑二分哪些部分的答案是一样的,只会进行 次二分,时间复杂度为 。
取 ,理论最优时间复杂度为 。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int BLOCK_SIZE = 600;
int n, ans[100005];
int fa[100005], num, idx[100005];
int f[100005];
vector<int> G[100005];
void dfs(int x, int fa) {
::fa[x] = fa;
for (int y : G[x]) if (y != fa) dfs(y, x);
idx[++num] = x;
}
int solve(int k) {
int res = 0; f[0] = -1;
for (int i = 1; i <= n; ++i) f[i] = 1;
for (int i = 1; i <= n; ++i) {
int x = idx[i];
if (f[fa[x]] != -1 && f[x] != -1) {
if (f[x] + f[fa[x]] >= k) ++res, f[fa[x]] = -1;
else f[fa[x]] = max(f[fa[x]], f[x] + 1);
}
}
return res;
}
int main(void) {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v; scanf("%d%d", &u, &v);
G[u].emplace_back(v); G[v].emplace_back(u);
} ans[1] = n; dfs(1, 0);
for (int k = 2; k <= BLOCK_SIZE; ++k) ans[k] = solve(k);
for (int k = BLOCK_SIZE + 1; k <= n; ) {
int L = k - 1, R = n + 1, res = solve(k);
while (L + 1 != R) {
int mid = L + R >> 1;
if (res == solve(mid)) L = mid;
else R = mid;
}
for (int i = k; i <= L; ++i) ans[i] = res;
k = R;
}
for (int i = 1; i <= n; ++i) printf("%d\n", ans[i]);
return 0;
}