Phrvth量子力学对不起,爱因斯坦

算法思想

Hi,Phrvth·2023-04-09 17:03·58 次阅读

算法思想

Part 1. 前提提要#

注意:本文为提高组难度的算法思想,主要为前缀和,差分等优化

因为是思想,讲的会比较玄乎,能理解就好

Part 2. 双指针#

双指针通常解决区间问题

步骤是,确定一个右节点或左节点作为一个参考点,通常取右节点,记为 j

我们考虑一个刚好符合题目条件的最小节点,记为 l

双指针有个前提:当右指针往右移动时,左指针不可能往前走

解决步骤:

右指针往右跳,让左指针也往右跳,跳到符合条件为止,这就是题目要求的以 r 为右端点最长的区间

因为左指针和右指针都只会跳 n 次,总时间复杂度为 O(n)

Practice 1.#

P1638逛画展

Practice 2.#

P1102 A-B数对

Part 3. 前缀和#

一维前缀和#

我们有一个序列 A,我们要进行一下操作

  • O(n) 地预处理
  • 给定一个 lr,求 lirAi,时间复杂度为 O(1)

我们记一个数组 B 满足

Bi=1jiAj

预处理 B 数组,时间复杂度为 O(n)

我们要查询 [l,r] 的值,也就是 lirAi,经过简单数学推导,如下

lirAi=1jrAj1k<lAk=BrBl1

通俗点来说,就是 [l,r] 的值等于 BrBl1

前缀和在众多领域起着重大作用,以代码量小和快著称

弊端是:不能动态修改

二维前缀和#

给定一个二维序列,记为 A,现要支持

  • 给定两点 (l1,r1)(l2,r2),求以两点为矩形端点的矩形,里面所有数之和

记一个二维数组 Bi,j(1,1)(i,j) 的数之和

初始化 Bi,j=Bi1,j+Bi,j1Bi1,j1+Aij

证明平凡,自己画图

则两点 (l1,r1)(l2,r2),求以两点为矩形端点的矩形,里面所有数之和就为:

Bl2,r2Bl11,r2Bl2,r11+Bl11,r21

推导过程平凡,请自己画图证明

多维前缀和#

类比容斥原理

Practice#

见题单

Part 4. 差分#

一维差分#

现有一个序列 A,先要支持

  • 动态修改 [l,r] 的值
  • O(n) 打印 A 序列的每个数

记一个数组 B 满足

Bi=AiAi1

我们看看 Ai 等与什么

1jiBj=1jiAjAj1=Ai

也就是说,差分的前缀和等于原数组,简单的想,前缀和的差分等于差分

这三者就有机的连在一起了

接下来看怎么修改,记修改的数为 k

  • i=l 时,Bi=(Ai+k)Ai1=Bi+k
  • i=r+1 时, Bi=(Ar+1(Ar+k))=Bik
  • i(l,r] 时,Bi=(Ai+k)(Ai1+k)=Bi

通俗点说,就是修改 [l,r] 的值时,只需要修改 BlBr+1 的值即可

时间复杂度为:O(1)

二维差分#

类比 Si,j=Si1,j+Si,j1Si1,j1+Aij

因为 B 的前缀和为 A,则用 A 替代 SB 替代 A

Ai,j=Ai1,j+Ai,j1Ai1,j1+Bi,jBij=Ai,jAi1,jAi,j1+Ai1,j1

这就是二维差分的初始化了

我们考虑修改 (u,v)(n,n) 的差分

  • (i,j)=(u,v) 时,Bi,j=(Ai,j+w)Ai1,jAi,j1+Ai1,j1=Bi,j+w

可以证明,其他位置的差分值不变

可以推导 (u,v)(x,y) 的差分值为

Bu,v+w,Bu,y+1w,Bx+1,vw,Bx+1,y+1+w

证明平凡,类似后缀和

多维差分#

和二维差分的推导过程一样

Practice#

见题单

Part 5.离散化#

我们要将一个值域为 (,) 的数映射到 (0,n] 之中

怎么做到呢?

我们知道,映射有个性质

  • 每个数的大小关系不变

想到什么?排名是不是可以完美做到这一点?

所以,我们第一步是将原数组去重排序+排序

排序用 sort 人尽皆知,去重用什么呢?

使用 unique 函数,使用格式: unique(a.begin(),a.end())这里 a.begin(),a.end() 分别指数组的头指针和尾指针,普通数组用 a+1,a+1+n

  • 注意一点:该函数的返回值是不属于去重数组的第一个地址,比如有个数组长度为 n ,去重数组长度为 k ,他的返回值就是 ak+1的地址。

根据上面几点,我们可以推出公式。

k=unique(a+1,a+1+n)-(a+1)

这样我们就可以求出去重数组的长度了。

第二步,我们求排名

设这个数为 AiB 数组的排名为 k,则他在 A 数组的排名为 k

那怎么找 i 呢?这里使用一个函数:

lower_bound(a+1,a+1+n,x) 他返回的是第一个大于等于 x 的元素的地址,注意:必须有序

于是,我们可以推出公式:

rk[i] = lower_bound(b + 1, b + 1 + k, a[i]) - b;

到此结束

因为 rki 存的是第 aib 数组的下标,则

Brki=Ai

Copy
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] << ' '; }

Practice#

见题单

Part 6. 位运算#

基本位运算#

  • 按位与:x&y,把两数的对应位做逻辑与
  • 按位或:x|y,把两位的对应位做逻辑或
  • 按位异或:xy,把两位对应位做逻辑异或
  • 按位取反:!x,把 x 的每一位取逻辑非
  • 左移:x<<y,把 x 的整体向左移动 y 位,高位抹去,低位补0,(x<<y=x2y1
  • 右移:x>>y,把 x 的整体向右移动 y 位,低位抹去,高位补0,(x>>y=x2y1)(向下取整)

单点修改:

  • 将第 i 位修改为 1x |= 1<<(i-1)
  • 保证第 i 位为 1,修改为 0x^=1<<(i-1)

注意:<<>> 的优先级比 低,则:1<<(i1)=1<<i1

求两个集合的交集,并集和对称差#

可以把两个集合看成一个二进制数,则

xi=0 表示 i 不在集合中,反之同理

两个二进制的与,或,异或可以达到需要效果

神器!bitset#

用二进制表示集合时,普通的变量存不下这么大的二进制,我们可以使用 bitset 创建一个超长二进制

支持:所有位运算操作也支持修改这个串的某一位

时间复杂度为:O(nw)w 通常取 64

基本用法:

  • bitset<N> s 表示创建一个二进制数
  • s.set(i,0/1) 表示在第 i 位设置成 0/1
  • s.test(i) 返回第 i 位的值
  • s.count() 返回 1 的数量
  • s.reset()s 清空成 0
  • s.flip() 对二进制取反
  • 可以直接进行逻辑运算

注意:声明 bitset 时,N 必须是常量(CE不关我事(doge.

Practice#

见题单

Part 7. 单调数据结构#

单调栈#

单调栈维护的是一个栈,里面的元素单调递增或单调递减

用于找后缀最大值。

定义:当 i 为后缀最大值时,当且仅当:对于所有的 i<j|A|,都有 Ai>Aj

因为栈内数据单调递减,所以对于一个在栈内的元素 Ai,他后面没有比他大的数(不然就被弹出了

所以留在栈内的都是后缀最大值

时间复杂度为:O(n)

单调队列#

单调队列维护的是一个队列,只不过里面的数的下标都严格在一个区间里

比你小的人都打不过,你应该出队

被单调队列了

Practice#

题比较难,所以我写了几篇题解,自己去看

Part 8.倍增#

倍增基本概念#

我们先来考虑一个引论

  • 任何一个 xN,都可以拆成若干个 2 的正整数次幂之和

n=2i(iN)

证明平凡

所以对于每一段区间,我们都可以拆成 logn 个区间

倍增快速幂#

我们要求 xymodp 的值

x,y109

怎么办呢?

我们考虑使用倍增的思路:

xy=(x2)y2

这里需要进行奇偶性判断,如果是奇数,则直接乘进答案中即可

时间复杂度为:O(logx)

Code#

Copy
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; }

Practice#

见题单

ST表#

ST表是什么呢?他是专门解决 RMQ 问题的一个简便写法(比线段树好写多了

RNQ 问题

O(nlogn) 的预处理,要求支持:

  • 给定 l,r 要求 maxlir{ai}O(1) 的时间复杂度

根据我们倍增的知识,我们可以定一个 st 表,如下

记:fi,j 表示 [i,i+2j1] 这段区间的最大值,用人话来讲就是从第 i 为开始,连续 2j 个数

状态转移方程也很好推:

fi,j=max{fi,j1,fi+2j1,j1}

怎么记呢?爸爸的爸爸叫爷爷

是不是很好记(doge.

时间复杂度为 O(nlogn)

那怎么查询呢?

可以用我们上面证的,一段区间可以拆成若干个2的正整数次幂长度的区间,对这个区间进行二进制分解即可

但这样的时间复杂度为 O(logn)

因为是最大值,所以比较区间可以重叠

这一段区间可以跳 logn 次,所以我们就左端点跳 logn 次,右端点跳 logn 次,这样虽有重叠,也无伤大雅

时间复杂度为 O(1)

Code#

Copy
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"; }

Practice#

见题单

Part 10.分治#

简述分治算法#

简单来说,就是原问题分成两个差不多一样的问题,先解决他们,在返回来解决自己

重要的是:你在做这一层的时候,你不要管下去后他怎么了,不然会晕的,把他默认成已知就行

基本步骤

  • "分":如何把大问题划分为更小的答案
  • "合":如何把小问题合并成更大的答案

归并排序#

  • “分“

很显然,当前区间的子任务就是长度一半的区间,即分成两个区间长度都为当前区分的一半

  • "合"

重要部分是 “合”,即现在有两个有序的序列,要把他们合并到一个大的序列里,怎么做呢?

有序表合并,可以用我们刚讲过的双指针思想

即记 iA 序列的起点,j 为序列 B 的起点

现在比较 AiBj

  • Ai>Bj,就将 Bj 加到答案序列里,同时 j 自增
  • Ai<Bj,就将 Ai 加到答案序列里,同时 i 自增

最后如果 i 或者 j 没有跑完,就把后面的数直接加进去即可

注意到,答案序列只在原来区间的位置,所以起点和终点就是 l,r

Code#

Copy
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]; }

归并排序解决逆序对问题#

逆序对问题

现求 A 序列中满足 (i,j)i<jAi>Aj(i,j) 的数量

对于 [l,r] 的区间,我们分治的去考虑问题,逆序对只有可能出在三种情况

  • 全部出现在左区间
  • 全部出现在右区间
  • 横跨在两个区间之间

前两个操作可以在递归中完成

我们主要看第三个部分

iA 序列的指针, jB 序列的指针

还是分类讨论

  • Ai>Bj,说明 Ain 的数都比 Bj 大,把后面的答案统计一下就行,再将 Bj 加到答案序列里,同时 j 自增
  • Ai<Bj,就将 Ai 加到答案序列里,同时 i 自增

最后如果 i 或者 j 没有跑完,就把后面的数直接加进去即可

Code#

Copy
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分治#

这东西,有亿点恶心,耗了我两年半进行理解……

一维偏序#

排序即可

二维偏序#

例子:逆序对

他就是用归并排序解决的一个二维偏序问题

例子:树状数组

现有一个序列,要支持两种操作

  • 给定 x,k 要求 Ax 加上 k (单点修改
  • 给定 x,y,要求 xiyAi(区间查询

分析:

如果你要用数据结构,我也拦不到你

我们考虑二维偏序

记一个多元祖 (t,x,w,op)t 为操作编号,x 为操作位置,w 为修改值,op 为对答案的贡献倍数(1/-1)

对于一个查询操作 i,其结果就是所有满足 tj<tipj<pi 的查询 w 之和,类似顺序对问题

因为前面的数天生满足 tj<ti,我们只需要找到 pj<piwj 之和

考虑分治

solve(l,r) 表示 [l,r] 操作的贡献,solve(l,mid)solve(mid+1,r) 时已知,因为左区间和右区间都已经满足两个条件

所以双指针即可

这里给上代码:

Copy
#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 等方式解决。

n 个元素,第 i 个元素有 ai,bi,ci 三个属性,设 f(i) 表示满足 ajaibjbicjcijij 的数量。

对于 d[0,n),求 f(i)=d 的数量。

1n1051ai,bi,cik2×105

解决:

第一步,先按第一关键字排序(sort)

第二步:用cdq对第二关键字排序

第三步:用树状数组维护第三关键字(类似用树状数组维护逆序对)

具体的和一些细节:

我们先用 sort 对第一关键字进行排序,保证每个符合条件的 j 都在 i 的右边

我们直接上cdq对第二关键字排序

最后用树状数组就行

如果有重复的情况怎么办?

考虑到有可能有 ji 的右边,导致没算全,但是最右边的 i 是绝对正确的,所以我们可以把序列排序,然后把连着的相同的全部赋值成最后一个数即可

注意细节

Copy
#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时,我们发现第一维被打乱了

但是我们只需要的知道他在左边还是右边,所以我们就可以记录第一维的 LEFTRIGHT

再对第三位进行cdq,第四维用树状数组维护即可

偏序的时间复杂度#

用主定理可以求解

逆序对是 O(Nlogn)

三维偏序是 O(Nlog2n)

k 维偏序时 O(Nlogk1n)

Part 11. 尾声#

算法思想多加练习即可

posted @   Phrvth  阅读(58)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
目录
?