算法思想
1. 前提提要#
注意:本文为提高组难度的算法思想,主要为前缀和,差分等优化
因为是思想,讲的会比较玄乎,能理解就好
2. 双指针#
双指针通常解决区间问题
步骤是,确定一个右节点或左节点作为一个参考点,通常取右节点,记为
我们考虑一个刚好符合题目条件的最小节点,记为
双指针有个前提:当右指针往右移动时,左指针不可能往前走
解决步骤:
右指针往右跳,让左指针也往右跳,跳到符合条件为止,这就是题目要求的以
因为左指针和右指针都只会跳
1.#
2.#
3. 前缀和#
一维前缀和#
我们有一个序列
地预处理- 给定一个
和 ,求 ,时间复杂度为
我们记一个数组
预处理
我们要查询
通俗点来说,就是
前缀和在众多领域起着重大作用,以代码量小和快著称
弊端是:不能动态修改
二维前缀和#
给定一个二维序列,记为
- 给定两点
和 ,求以两点为矩形端点的矩形,里面所有数之和
记一个二维数组
初始化
证明平凡,自己画图
则两点
推导过程平凡,请自己画图证明
多维前缀和#
类比容斥原理
#
见题单
4. 差分#
一维差分#
现有一个序列
- 动态修改
的值 打印 序列的每个数
记一个数组
我们看看
也就是说,差分的前缀和等于原数组,简单的想,前缀和的差分等于差分
这三者就有机的连在一起了
接下来看怎么修改,记修改的数为
- 当
时, - 当
时, - 当
时,
通俗点说,就是修改
时间复杂度为:
二维差分#
类比
因为
这就是二维差分的初始化了
我们考虑修改
- 当
时,
可以证明,其他位置的差分值不变
可以推导
证明平凡,类似后缀和
多维差分#
和二维差分的推导过程一样
#
见题单
5.离散化#
我们要将一个值域为
怎么做到呢?
我们知道,映射有个性质
- 每个数的大小关系不变
想到什么?排名是不是可以完美做到这一点?
所以,我们第一步是将原数组去重排序+排序
排序用 sort
人尽皆知,去重用什么呢?
使用 unique 函数,使用格式: unique(a.begin(),a.end())
这里 a.begin(),a.end()
分别指数组的头指针和尾指针,普通数组用 a+1,a+1+n
- 注意一点:该函数的返回值是不属于去重数组的第一个地址,比如有个数组长度为
,去重数组长度为 ,他的返回值就是 的地址。
根据上面几点,我们可以推出公式。
k=unique(a+1,a+1+n)-(a+1)
这样我们就可以求出去重数组的长度了。
第二步,我们求排名
设这个数为
那怎么找
lower_bound(a+1,a+1+n,x)
他返回的是第一个大于等于
于是,我们可以推出公式:
rk[i] = lower_bound(b + 1, b + 1 + k, a[i]) - b;
到此结束
因为
sort(b + 1, b + 1 + n);
int *ed = unique(b + 1, b + 1 + n);
for (int i = 1; i <= n; i++) {
rk[i] = lower_bound(b + 1, ed, a[i]) - (b + 1) + 1;
cout << rk[i] << ' ';
}
#
见题单
6. 位运算#
基本位运算#
- 按位与:
,把两数的对应位做逻辑与 - 按位或:
,把两位的对应位做逻辑或 - 按位异或:
,把两位对应位做逻辑异或 - 按位取反:
,把 的每一位取逻辑非 - 左移:
,把 的整体向左移动 位,高位抹去,低位补0,( ) - 右移:
,把 的整体向右移动 位,低位抹去,高位补0,( )(向下取整)
单点修改:
- 将第
位修改为 :x |= 1<<(i-1)
- 保证第
位为 ,修改为 :x^=1<<(i-1)
注意:
求两个集合的交集,并集和对称差#
可以把两个集合看成一个二进制数,则
两个二进制的与,或,异或可以达到需要效果
神器! #
用二进制表示集合时,普通的变量存不下这么大的二进制,我们可以使用
支持:所有位运算操作也支持修改这个串的某一位
时间复杂度为:
基本用法:
bitset<N> s
表示创建一个二进制数s.set(i,0/1)
表示在第 位设置成s.test(i)
返回第 位的值s.count()
返回 1 的数量s.reset()
把 清空成 0s.flip()
对二进制取反- 可以直接进行逻辑运算
注意:声明
#
见题单
7. 单调数据结构#
单调栈#
单调栈维护的是一个栈,里面的元素单调递增或单调递减
用于找后缀最大值。
定义:当
因为栈内数据单调递减,所以对于一个在栈内的元素
所以留在栈内的都是后缀最大值
时间复杂度为:
单调队列#
单调队列维护的是一个队列,只不过里面的数的下标都严格在一个区间里
比你小的人都打不过,你应该出队
被单调队列了
#
题比较难,所以我写了几篇题解,自己去看
8.倍增#
倍增基本概念#
我们先来考虑一个引论
- 任何一个
,都可以拆成若干个 的正整数次幂之和
证明平凡
所以对于每一段区间,我们都可以拆成
倍增快速幂#
我们要求
怎么办呢?
我们考虑使用倍增的思路:
这里需要进行奇偶性判断,如果是奇数,则直接乘进答案中即可
时间复杂度为:
#
typedef long long ll;
ll qpow(ll d, ll b, ll Mod) {
ll ans = 1;
while (b) {
if (b & 1) ans = ans * d % Mod;
d >>= 1; b = b * b % Mod;
}
return ans;
}
#
见题单
ST表#
ST表是什么呢?他是专门解决 RMQ 问题的一个简便写法(比线段树好写多了
RNQ 问题
- 给定
要求 , 的时间复杂度
根据我们倍增的知识,我们可以定一个 st 表,如下
记:
状态转移方程也很好推:
怎么记呢?爸爸的爸爸叫爷爷!
是不是很好记(doge.
时间复杂度为
那怎么查询呢?
可以用我们上面证的,一段区间可以拆成若干个2的正整数次幂长度的区间,对这个区间进行二进制分解即可
但这样的时间复杂度为
因为是最大值,所以比较区间可以重叠!
这一段区间可以跳
时间复杂度为
#
n = read(), m = read();
for (int i = 1; i <= n; i++) f[0][i] = read();
for (int t = 1; (1 << t) <= n; t++)
for (int i = 1; i + (1 << t) - 1 <= n; i++)
f[t][i] = max(f[t - 1][i], f[t - 1][i + (1 << t - 1)]);//注意优先级的问题
for (int l, r; m; m --) {
l = read(), r = read();
int len = r - l + 1, t = log2(len);
cout << max(f[t][l], f[t][r - (1 << t) + 1]) << "\n";
}
#
见题单
10.分治#
简述分治算法#
简单来说,就是原问题分成两个差不多一样的问题,先解决他们,在返回来解决自己
重要的是:你在做这一层的时候,你不要管下去后他怎么了,不然会晕的,把他默认成已知就行
基本步骤
- "分":如何把大问题划分为更小的答案
- "合":如何把小问题合并成更大的答案
归并排序#
- “分“
很显然,当前区间的子任务就是长度一半的区间,即分成两个区间长度都为当前区分的一半
- "合"
重要部分是 “合”,即现在有两个有序的序列,要把他们合并到一个大的序列里,怎么做呢?
有序表合并,可以用我们刚讲过的双指针思想
即记
现在比较
,就将 加到答案序列里,同时 自增 ,就将 加到答案序列里,同时 自增
最后如果
注意到,答案序列只在原来区间的位置,所以起点和终点就是
#
void solve (int l, int r) {
if (l == r) return ;
int mid = l + r >> 1;
solve(l, mid); solve(mid + 1, r);//分
int i = l, j = mid + 1, pos = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) b[pos++] = a[i++];
else b[pos++] = a[j++];
}
while (i <= mid) b[pos++] = a[i++];
while (j <= r) b[pos++] = a[j++];
for (int i = l; i <= r; i++) a[i] = b[i];
}
归并排序解决逆序对问题#
逆序对问题:
现求
对于
- 全部出现在左区间
- 全部出现在右区间
- 横跨在两个区间之间
前两个操作可以在递归中完成
我们主要看第三个部分
令
还是分类讨论
,说明 的数都比 大,把后面的答案统计一下就行,再将 加到答案序列里,同时 自增 ,就将 加到答案序列里,同时 自增
最后如果
#
void solve (int l, int r) {
if (l == r) return ;
int mid = l + r >> 1;
solve(l, mid); solve(mid + 1, r);
int i = l, j = mid + 1, pos = l;
while (i <= mid && j <= r) {
if (a[i] <= a[j]) b[pos++] = a[i++];
else b[pos++] = a[j++], ans += mid - i + 1;//改动
}
while (i <= mid) b[pos++] = a[i++];
while (j <= r) b[pos++] = a[j++];
for (int i = l; i <= r; i++) a[i] = b[i];
}
CDQ分治#
这东西,有亿点恶心,耗了我两年半进行理解……
一维偏序#
排序即可
二维偏序#
例子:逆序对
他就是用归并排序解决的一个二维偏序问题
例子:树状数组
现有一个序列,要支持两种操作
- 给定
要求 加上 (单点修改 - 给定
,要求 (区间查询
分析:
如果你要用数据结构,我也拦不到你
我们考虑二维偏序
记一个多元祖
对于一个查询操作
因为前面的数天生满足
考虑分治
设
所以双指针即可
这里给上代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e6 + 7;
struct Node{
int type, x, v, id;
bool operator < (const Node other) const {
return x == other.x ? type < other.type : x < other.x;//注意,先修改后查询
}//现区间没影响,合起来之后有影响
}A[MAXN], B[MAXN];
int n, m, cnt, Ans[MAXN], arr;
void CDQ(int l, int r) {
if (l == r) return ;
int mid = l + r >>1;
CDQ(l, mid), CDQ(mid + 1, r);
int i = l, j = mid + 1, sum = 0, tot = l;
while (i <= mid && j <= r) {
if (A[i] < A[j]) {
if (A[i].type == 1) sum += A[i].v;//双指针:让左节点一直跳,跳到不符合条件即可
B[tot ++] = A[i ++];
} else {
if (A[j].type == 2) Ans[A[j].id] -= sum;//跳完之后减上贡献
else if (A[j].type == 3) Ans[A[j].id] += sum;//或加
B[tot ++] = A[j ++];
}//记得是累加哦
}
while (i <= mid) B[tot ++] = A[i ++];
while (j <= r) {
if (A[j].type == 2) Ans[A[j].id] -= sum;//累加即可
if (A[j].type == 3) Ans[A[j].id] += sum;
B[tot ++] = A[j ++];
}
for (int i = l; i <= r; i ++) A[i] = B[i];
}
int main () {
cin >> n >> m;
for (int i = 1, x; i <= n; i ++) cin >> x, A[++ cnt].type = 1, A[cnt].x = i, A[cnt].v = x;
for (int i = 1; i <= m; i ++) {
int op, x, k;
cin >> op >> x >> k;
if (op == 1) {
A[++ cnt].type = 1, A[cnt].x = x, A[cnt].v = k;
} else {
A[++ cnt].type = 2, A[cnt].x = x - 1, A[cnt].id = ++ arr;//arr需相等
A[++ cnt].type = 3, A[cnt].x = k, A[cnt].id = arr;
}
}
CDQ(1, cnt);
for (int i = 1; i <= arr; i ++) cout << Ans[i] << '\n';
return 0;
}
三维偏序#
题目:
这是一道三维偏序模板题,可以使用 bitset,CDQ 分治,KD-Tree 等方式解决。
有
对于
解决:
第一步,先按第一关键字排序(sort)
第二步:用cdq对第二关键字排序
第三步:用树状数组维护第三关键字(类似用树状数组维护逆序对)
具体的和一些细节:
我们先用 sort 对第一关键字进行排序,保证每个符合条件的
我们直接上cdq对第二关键字排序
最后用树状数组就行
如果有重复的情况怎么办?
考虑到有可能有
注意细节
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 7;
const int MAXM = 2e5 + 7;
struct Node{
int a, b, c, id;
bool operator < (const Node other) const {
if (a != other.a) return a < other.a;
if (b != other.b) return b < other.b;
if (c != other.c) return c < other.c;
return id < other.id;
}
}A[MAXN], B[MAXN];
int n, k;
int D[MAXM], K[MAXN], H[MAXN];
namespace BIT {
#define lb(i)(i & -i)
void modify(int x, int v) {
for (; x <= k; x += lb(x)) D[x] += v;
}
void query(int x, int &r) {
for (; x; x -= lb(x)) r += D[x];
}
}
void CDQ(int l, int r) {
if (l == r) return ;
int mid = l + r >> 1;
CDQ(l, mid); CDQ(mid + 1, r);
int i = mid + 1, j = l, tot = l;
while (i <= r && j <= mid) {
if (A[i].b >= A[j].b) //符合,考虑相等情况
BIT :: modify(A[j].c, 1), B[tot ++] = A[j ++];
else
BIT :: query(A[i].c, K[A[i].id]), B[tot ++] = A[i ++];//k数组没有顺序问题,只是一个容器
}
while (j <= mid) BIT ::modify(A[j].c, 1), B[tot ++] = A[j ++];//注意到,左半部分只会加1,所以清零时免得清成负数
while (i <= r) BIT :: query(A[i].c, K[A[i].id]), B[tot ++] = A[i ++];//剩下的都是可以
for (int i = l; i <= mid; i ++) BIT :: modify(A[i].c, -1);
for (int i = l; i <= r; i ++) A[i] = B[i];
}
int main () {
ios :: sync_with_stdio(false); cin.tie(NULL);
cin >> n >> k;
for (int i = 1; i <= n; i ++) cin >> A[i].a >> A[i].b >> A[i].c, A[i].id = i;
sort(A + 1, A + 1 + n);
CDQ(1, n);
sort(A + 1, A + 1 + n);
for (int i = n; i >= 1; i --) {
if (A[i].a == A[i + 1].a && A[i].c == A[i + 1].c && A[i].b == A[i + 1].b) //完全一样
K[A[i].id] = K[A[i + 1].id];
//排序之后,相等的数肯定是连在一起的
//对一维排序的原因是让i的符合条件的j都在i的左边,相等的情况j在i的右边我们统计不到,但是最后的那个数一定正确
H[K[A[i].id]] ++;
}
for (int i = 0; i < n; i ++) cout << H[i] << '\n';
return 0;
}
更高维的偏序#
以四维偏序为例,我们按第一关键字排序
给第二关键字cdq时,我们发现第一维被打乱了
但是我们只需要的知道他在左边还是右边,所以我们就可以记录第一维的
再对第三位进行cdq,第四维用树状数组维护即可
偏序的时间复杂度#
用主定理可以求解
逆序对是
三维偏序是
11. 尾声#
算法思想多加练习即可
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现