集训day1&day2(单调队列,单调栈,ST表,LCA,线段树,二分,三分)
还有好多题目没写呢,可能会持续更新~
【模板】离线二维数点
题意:给出二维平面上一些点,求 \(x\) 坐标 \([l, r]\),\(y\) 坐标 \([0, x]\) 之内的点的个数。
很容易发现,可以对 \(x\) 坐标进行前缀和,得到 \(ans(l, r) = ans(0, r) - ans(0, l-1)\),对于 \(ans(0, k)\),我们可以对每个问题离线下来按右端点排序,用一个扫描线从左到右扫过去,对 \(y\) 坐标创建一个树状数组(单点加一和区间查询),就搞定啦~
注意不要开 long long,否则你把快读和快写都加上也会 TLE。
简简单单~~~
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e6 + 5;
inline int read() {
int x=0,f=1;char ch=getchar();
while (ch<'0' || ch>'9') {if(ch=='-')f=-1; ch=getchar();}
while (ch>='0' && ch<='9') {x=x*10+ch-'0'; ch=getchar();}
return f*x;
}
struct Fenwick_Tree {
int tr[maxn];
int lowbit(int x) {return x & (-x);}
void add(int x) {
for (int i = x;i <= maxn;i += lowbit(i))
tr[i]++;
}
int query(int x) {
int res = 0;
for (int i = x;i >= 1;i -= lowbit(i))
res += tr[i];
return res;
}
}tr;
struct Node {
int x, id, val;
};
int n, m, a[maxn], ans[maxn];
vector <Node> v[maxn];
signed main()
{
n=read(), m=read();
for (int i = 1;i <= n;i++) {
a[i] = read();
}
for (int i = 1;i <= m;i++) {
int l=read(), r=read(), x=read();
v[l - 1].push_back({x, i, -1});
v[r].push_back({x, i, 1});
}
for (int i = 1;i <= n;i++) {
tr.add(a[i]);
for (int j = 0;j < v[i].size();j++) {
ans[v[i][j].id] += v[i][j].val * tr.query(v[i][j].x);
}
}
for (int i = 1;i <= m;i++) {
cout << ans[i] << '\n';
}
return 0;
}
【模板】ST表&RMQ
模板好吧懒得讲了
这个更简单
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 10;
int st[MAXN][21];
int query(int l, int r) {
int k = log2(r - l + 1);
return max(st[l][k], st[r - (1 << k) + 1][k]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> st[i][0];
}
for (int j = 1; j <= 21; j++) {
for (int i = 1; i + (1 << j) - 1 <= n; i++) {
st[i][j] = max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
}
while (m--) {
int l, r;
cin >> l >> r;
cout << query(l, r) << "\n";
}
return 0;
}
【模板】三分
这个也是懒得讲了好吧
这么简单的题你还要看代码吗
#include <bits/stdc++.h>
#define int long long
using namespace std;
const double eps = 1e-6;
double l, r, a[15];
int n;
double calc(double x) {
double sum = 0;
for (int i = n;i >= 0;i--) {
sum = sum * x + a[i];
}
return sum;
}
signed main()
{
cin >> n >> l >> r;
for (int i = n;i >= 0;i--) {
cin >> a[i];
}
while (fabs(l - r) >= eps) {
double mid = (l + r) / 2;
if (calc(mid + eps) > calc(mid - eps)) l = mid;
else r = mid;
}
printf("%.5lf", r);
return 0;
}
[HEOI2016/TJOI2016] 排序
非常好的一道珂朵莉树线段树 + 二分的题目。
首先我们发现直接做不好做,因为没有任何一种算法和数据结构针对排序(除了珂朵莉树),而且这道题是一个区间修改 + 单点查询,是不是可以用其它的方式去替代排序?如果我们加一个特殊性质:给出的数列只有 \(0\) 和 \(1\),那么升序排序就是把 \(0\) 全排到前面,\(1\) 全排到后面,形式化地,如果 \(1\) 的个数是 \(cnt_1\),则升序排序相当于把 \([l, r-cnt_1]\) 的全改成 \(0\),\([l+cnt_1, r]\) 的全改成 \(1\),降序也是同样的原理。
又注意到只查询 \(1\) 次,这意味着我们不需要极多地构建线段树,再结合之前的想法,答案就出——来——了——:直接二分答案为 \(mid\),大于等于 \(mid\) 的数字视为 \(1\),否则视为 \(0\),对于排序直接用线段树即可,查询即为判断这个点是否为 \(1\):如果为 \(1\),就表示实际答案 \(\ge mid\),否则表示实际答案 \(< mid\),很容易发现,这个方法是满足单调性的。
AC code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e5 + 5;
int n, m, a[maxn], op[maxn], L[maxn], R[maxn], q, ans = 0;
inline int read() {
int x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
return f * x;
}
struct Segment_Tree {
#define mid ((l + r) >> 1)
#define lson i << 1, l, mid
#define rson i << 1 | 1, mid + 1, r
int tr[4 * maxn], tag[4 * maxn];
void push_up(int i) {
tr[i] = tr[i << 1] + tr[i << 1 | 1];
}
void build(int i, int l, int r, int x) {
tag[i] = 0;
if (l == r) {
tr[i] = (a[l] >= x);
return;
}
build(lson, x);
build(rson, x);
push_up(i);
}
void addtag(int i, int l, int r, int d) {
tag[i] = d;
if (d == -1) tr[i] = 0;
else if (d == 1) tr[i] = r - l + 1;
}
void push_down(int i, int l, int r) {
if (tag[i] != 0) {
addtag(lson, tag[i]);
addtag(rson, tag[i]);
tag[i] = 0;
}
}
int query(int i, int l, int r, int pl, int pr) {
if (pl <= l && r <= pr) return tr[i];
push_down(i, l, r);
int res = 0;
if (pl <= mid) res += query(lson, pl, pr);
if (pr > mid) res += query(rson, pl, pr);
return res;
}
int query_node(int i, int l, int r, int x) {
if (l == x && r == x) return tr[i];
push_down(i, l, r);
if (x <= mid) return query_node(lson, x);
else return query_node(rson, x);
}
void update(int i, int l, int r, int pl, int pr, bool d) {
if (pl <= l && r <= pr) {
tr[i] = d * (r - l + 1);
tag[i] = (d == 0 ? -1 : 1);
return;
}
push_down(i, l, r);
if (pl <= mid) update(lson, pl, pr, d);
if (pr > mid) update(rson, pl, pr, d);
push_up(i);
}
} tr;
bool check(int x) {
tr.build(1, 1, n, x);
for (int i = 1; i <= m; i++) {
int cnt1 = tr.query(1, 1, n, L[i], R[i]);
if (op[i] == 0) {
if (cnt1 > 0) tr.update(1, 1, n, R[i] - cnt1 + 1, R[i], 1);
if (R[i] - cnt1 >= L[i]) tr.update(1, 1, n, L[i], R[i] - cnt1, 0);
} else {
if (cnt1 > 0) tr.update(1, 1, n, L[i], L[i] + cnt1 - 1, 1);
if (L[i] + cnt1 <= R[i]) tr.update(1, 1, n, L[i] + cnt1, R[i], 0);
}
}
return tr.query_node(1, 1, n, q);
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= m; i++) op[i] = read(), L[i] = read(), R[i] = read();
q = read();
int l = 1, r = n, md;
while (l <= r) {
md = (l + r) >> 1;
if (check(md)) {
ans = md;
l = md + 1;
} else {
r = md - 1;
}
}
cout << ans << endl;
return 0;
}
[CSP-S 2022] 星战
借鉴了一下这篇题解(挺详细的,看不懂我的可以看这篇):https://www.cnblogs.com/crab-in-the-northeast/p/luogu-p8819.html
题意:
维护一个有向图 \(G = (V, E)\),初始有 \(n\) 个结点 \(m\) 条有向边(无重边)。每条边有 激活/失效 两种状态,初始全激活。
有四种动态操作(保证操作合法):
- 使边 \((u, v)\) 失效;
- 使以 \(u\) 为终点的所有边失效;
- 使边 \((u, v)\) 激活;
- 使以 \(u\) 为终点的所有边激活。
每次操作后,判断图是否同时满足:
- 每个结点出度均为 1(基环树森林)。
若满足则输出 YES,否则输出 NO。
发现出度直接弄的话第 \(2\)、\(4\) 操作都需要 \(O(n)\) 的时间复杂度,会 TLE,第一个想到的应该是任何图的入度之和 \(=\) 出度之和,所以我们先从入度入手,发现入度的所有操作都是 \(O(1)\) 的,而且如果所有出度为 \(1\),那么所有出度之和为 \(n\),那么入度之和也为 \(n\);但是如果入度之和为 \(n\),出度之和为 \(n\),每个出度不一定都为 \(1\)(可以自己举个例子)。
如何解决后者呢?这就是此题最妙的地方了:如果我们给每个节点设定一个随机化的权值 \(w[i]\),把入度之和换成带权的(即对于多条 \(u_i \to v\),设 \(r[v] = \sum_i w[u_i]\)),此时计算带权入度的复杂度依然是 \(O(1)\) 的,那么我们只需维护所有点的带权入度之和 \(\sum r(u)\),并与目标值 \(\sum w(u)\) 比较即可。
原理如下:
若每个点出度恰好为 \(1\),则每条边对应唯一一个起点 \(u\),该 \(w(u)\) 会恰好被计入一次(即计入其唯一出边终点的 \(r(v)\) 中),因此总和 \(\sum r(u) = \sum w(u)\)。
反之,若某点出度 \(k(u) \ne 1\),则 \(w(u)\) 会在总和中被计入 \(k(u)\) 次,导致 \(\sum r(u) = \sum k(u) \cdot w(u) \ne \sum w(u)\)。
虽然理论上存在非全 \(1\) 的 \(k(u)\) 使得等式仍成立,但由于 \(w(u)\) 是随机生成的大整数,这种碰撞概率极低,可忽略不计。
具体实现:
预处理每个点的初始带权入度 \(g[v] = \sum_{(u,v)\in E} w[u]\)。
维护当前带权入度数组 \(r[v]\) 和总和 \(\text{now} = \sum r[v]\)。
对四种操作分别更新:
- 操作 \(1\)(失活边 \((u,v)\)):\(r[v] \ -= w[u];\ \text{now} \ -= w[u];\)
- 操作 \(2\)(失活所有入边到 \(v\)):\(\text{now} \ -= r[v];\ r[v] = 0;\)
- 操作 \(3\)(激活边 \((u,v)\)):\(r[v] \ += w[u];\ \text{now} \ += w[u];\)
- 操作 \(4\)(激活所有入边到 \(v\)):\(\text{now} \ += g[v] - r[v];\ r[v] = g[v];\)
每次操作后判断 \(\text{now} == \text{target}\)(其中 \(\text{target} = \sum w[i]\)),输出 YES 或 NO。
时间复杂度:\(O(n + m + q)\),空间复杂度:\(O(n + m)\),轻松通过全部数据。
注:使用随机数可进一步降低哈希冲突概率,提高正确率。
一道有难度但代码很短的题
#include <bits/stdc++.h>
#define int long long
using namespace std;
inline int read() {
int x=0,f=1;char ch=getchar();
while (ch<'0' || ch>'9') {if(ch=='-')f=-1; ch=getchar();}
while (ch>='0' && ch<='9') {x=x*10+ch-'0'; ch=getchar();}
return f*x;
}
const int maxn = 5e5 + 5;
int n, m, r[maxn], w[maxn], g[maxn];
int sumw, sumr;
signed main()
{
n=read(), m=read();
srand(time(0) + 10);
for (int i = 1;i <= n;i++) {
w[i] = rand();
sumw += w[i];
}
while (m--) {
int u=read(), v=read();
r[v] += w[u];
g[v] = r[v];
sumr += w[u];
}
int q = read();
while (q--) {
int t = read(), u=read();
if (t == 1) {
int v=read();
r[v] -= w[u];
sumr -= w[u];
} else if (t == 2) {
sumr -= r[u];
r[u] = 0;
} else if (t == 3) {
int v = read();
r[v] += w[u];
sumr += w[u];
} else if (t == 4) {
sumr += g[u] - r[u];
r[u] = g[u];
}
cout << (sumr == sumw ? "YES\n" : "NO\n" );
}
return 0;
}

浙公网安备 33010602011771号