01解题技巧
1.位运算
- 位运算操作:
位运算符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两个位都是1,结果才是1 |
| | 或 | 两个位都是0,结果才是0 |
^ | 异或 | 两个位同0异1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进制位全部左移若干位,高位丢弃,低位补0(对于有符号数,最高位不变) |
>> | 右移 | 各二进制位全部右移若干位,算术右移最高位补符号位,逻辑右移最高位补0 |
- 小技巧:
位运算通常比算术运算要快,所以,当需要使用 2 i 2^i 2i时,可以用1<<i
代替;当需要将某一个数 a a a除以 2 i 2^i 2i时,可以用1>>i
代替。但是要注意,要打括号,因为问运算优先级在算术运算后面!!! - 用位运算取位存位方法:
操作 | 运算 |
---|---|
取出二进制数x的第k位 | (x>>k)&1 |
取出二进制数x的后k位(第0位到第k-1位) | x&((1<<k)-1) |
把二进制数x的第k位取反 | x^(1<<k) |
把二进制数x的第k位赋值为1 | x|(1<<k) |
把二进制数x的第k位赋值为0 | x&(~(i<<k)) |
- 常见用法
当需要使用二进制拆分(比如快速幂)时,用上面的位运算会更方便。
当把一个bool类型的标记数组压缩成一个二进制数时,也可以使用上面的位运算操作。 - lowbit函数
- 定义: l o w b i t ( x ) lowbit(x) lowbit(x)表示最低位的1以及后面的0构成的二进制数。
- 计算方法: l o w b i t ( x ) = n & ( − n ) lowbit(x)=n\&(-n) lowbit(x)=n&(−n)
2.离散化
- 作用:当某一个数组里的数值范围很大,但个数有限,并且只关心数之间的相对大小时,我们可以使用离散化把这些数对应成远比他们小的数,从而达到降低空间和时间复杂度的目的。
- 实现方法:
- 把目标数组里的所有数存到另一个辅助数组或vector容器中。
- 把辅助数组里的数进行排序,并且去掉重复数字(有重复数字会浪费对应的小数字)
- 把原数组中数的值改为对应成的数:使用二分查找到原数在辅助数组里的位置,并用其辅助数组的下标对应。
- 实现代码:
int n;//原有数的个数
int a[100001];//原数组
vector<int>lsh;//辅助数组
int main()
{
for(i=1;i<=n;i++)
{
lsh.push_back(a[i]);
}
sort(lsh.begin(),lsh.end());//从小到大排序
lsh.erase(unique(lsh.begin(),lsh.end()),lsh.end());//去重,并删除去掉的数
for(int i=1;i<=n;i++)//对应到原数组
{
a[i]=lower_bound(lsh.begin(),lsh.end(),a[i])-lsh.begin();//lower_bound可找出大于等于a[i]的第一个数,在这种情况中,找到的数就是a[i]。
}
}
3. 倍增
倍增与二进制划分比较像,它的意思就是字面意思——每次成倍增长。
具体的,我们要把一个做事次数
n
n
n,拆分成若干做事次数个数的和
n
=
n
1
+
n
2
+
n
3
+
.
.
.
+
n
i
n=n_1+n_2+n_3+...+n_i
n=n1+n2+n3+...+ni,而不是一次一次做这件事,这样做的目的是降低时间复杂度。
于是我们可以定义一个变量
k
k
k,表示每一次拆分出来的数字。初始时进行最小的拆分方法
k
=
1
k=1
k=1,尝试每一次拆分的值:若
k
k
k可以拆分出来则进行拆分并倍增一下——k=k*2
;否则不能拆分,就把k变小一点——k=k/2
。最后,当
k
=
0
k=0
k=0时,就说明不能再拆分了,结束程序。
这样做的时间复杂度就由
O
(
n
)
O(n)
O(n)降到
O
(
log
2
n
)
O(\log_2n)
O(log2n)了。
常见的应用有LCA(最近公共祖先)。
4. st表
st表示用来解决RMQ(区间最值问题)的,它也能降低时间复杂度(由 O ( n ) O(n) O(n)降到 O ( log 2 n ) O(\log_2n) O(log2n)),具体实现过程如下。
- 采用dp思想,定义 d p [ i ] [ j ] dp[i][j] dp[i][j]表示从第 i i i个位置开始的 2 j 2^j 2j个数的最值。
- 预处理:
- 初始值:每一个位置的最小值就是它本身,即
dp[i][0]=a[i]
。 - 状态转移:对于每层的
2
j
(
j
≥
1
)
2^j(j\geq1)
2j(j≥1)个数,可由前
2
j
−
1
2^{j-1}
2j−1个数和后
2
j
−
1
2^{j-1}
2j−1的最值合并得到,状态转移方程如下:
dp[i][j]=check(dp[i][j-1],dp[i+(1<<(j-1))][j-1])
,其中check()
表示求最值的函数,下同。
- 初始值:每一个位置的最小值就是它本身,即
- 询问答案:
- 若每次询问的区间为
[
l
,
r
]
[l,r]
[l,r]则可以将其分为两段,定义
k
=
⌊
log
2
(
r
−
l
+
1
)
⌋
k=\lfloor\log_2(r-l+1)\rfloor
k=⌊log2(r−l+1)⌋,则两段分别为区间的前
k
k
k个数和后
k
k
k个数。由于是求最值问题,中间有重合也没有关系。这样,我们就可以求出最值:
ans=check(dp[l][k],dp[r-(1<<k)+1][k])
,可参考下图。
- 若每次询问的区间为
[
l
,
r
]
[l,r]
[l,r]则可以将其分为两段,定义
k
=
⌊
log
2
(
r
−
l
+
1
)
⌋
k=\lfloor\log_2(r-l+1)\rfloor
k=⌊log2(r−l+1)⌋,则两段分别为区间的前
k
k
k个数和后
k
k
k个数。由于是求最值问题,中间有重合也没有关系。这样,我们就可以求出最值:
这样子,我们就可以用 O ( n × log 2 n ) O(n\times\log_2n) O(n×log2n)的复杂度进行预处理,然后用 O ( 1 ) O(1) O(1)的复杂度进行询问答案。
5. 前缀和与差分
- 前缀和
- 定义:对于一个数组
A
A
A,可求出它前
i
i
i项的和,即其前缀和数组
S
S
S:
S
[
i
]
=
∑
j
=
1
i
A
[
j
]
S[i]=\displaystyle\sum_{j=1}^{i}A[j]
S[i]=j=1∑iA[j],代码:
s[i]=s[i-1]+a[i]
(利用已经求出的 S [ i − 1 ] S[i-1] S[i−1])。 - 作用:
前缀和数组可以帮助我们快速求出数组中的一段连续区间的和: s u m ( l , r ) = ∑ i = l r A [ i ] = S [ r ] − S [ l − 1 ] sum(l,r)=\displaystyle\sum_{i=l}^{r}A[i]=S[r]-S[l-1] sum(l,r)=i=l∑rA[i]=S[r]−S[l−1]。若需要多次使用区间和,使用前缀和优化就可以降低时间复杂度。
- 定义:对于一个数组
A
A
A,可求出它前
i
i
i项的和,即其前缀和数组
S
S
S:
S
[
i
]
=
∑
j
=
1
i
A
[
j
]
S[i]=\displaystyle\sum_{j=1}^{i}A[j]
S[i]=j=1∑iA[j],代码:
- 二维前缀和
二维前缀和与前缀和相似,只是数组变成了二维的:- 求和公式:
S
[
i
]
[
j
]
=
∑
a
=
1
i
∑
v
=
1
j
A
[
a
]
[
b
]
S[i][j]=\displaystyle\sum_{a=1}^{i}\sum_{v=1}^{j}A[a][b]
S[i][j]=a=1∑iv=1∑jA[a][b],代码:
s[i][j]=a[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1]
,采用容斥原理得到,可借助下图理解:
- 求和公式:
S
[
i
]
[
j
]
=
∑
a
=
1
i
∑
v
=
1
j
A
[
a
]
[
b
]
S[i][j]=\displaystyle\sum_{a=1}^{i}\sum_{v=1}^{j}A[a][b]
S[i][j]=a=1∑iv=1∑jA[a][b],代码:
- 询问长方形的和:
sum(lx,ly,rx,ry)=s[rx][ry]-s[lx-1][ry]-s[rx][ly-1]+s[lx-1][ly-1]
,可通过上式变形或通过容斥原理得到。
- 差分
- 定义:对于一个数组 A A A,可求出它第 i i i项和第 i − 1 i-1 i−1项,即其差分数组 C C C: C [ i ] = A [ i ] − A [ i − 1 ] C[i]=A[i]-A[i-1] C[i]=A[i]−A[i−1]。
- 作用:在差分数组中,若将 C [ l ] ± t C[l]\pm t C[l]±t, C [ r + 1 ] ∓ t C[r+1]\mp t C[r+1]∓t,就相当于把原数组区间 [ l , r ] [l,r] [l,r]的值增加了 ± t \pm t ±t。若进行多次区间修改,差分可以降低时间复杂度。
6. 并查集
- 定义:并查集(Disjoint-Set)是一种可以动态维护若干个不重叠的集合、并支持合并与查询的数据结构。
- 问题描述:起初,一个元素就属于自己的一个小集合,给出一些关系 ( x , y ) (x,y) (x,y)表示将x和y所在的集合合并,给出一些询问 ( x , y ) (x,y) (x,y)表示询问元素x和y是否在同一个集合中。
- 并查集思路:
- 并查集使用代表元思想,即使用某一个固定元素代表整个集合:定义 F [ i ] F[i] F[i]表示 i i i号元素指向哪一个元素,即 i i i号元素与 F [ i ] F[i] F[i]号元素在一个集合里,每个集合是一棵逻辑上的树,合起来就是森林。
- 初始化:起初,每个元素都属于自己的一个小集合,即
F[i]=i
(自己指向自己表示自己就是集合的代表元)。 - 查询操作:对于每次查询,我们可以通过递归找到当前节点所在集合的代表元。若树退化成链,查询的复杂度就会变成
O
(
n
)
O(n)
O(n),可以进行以下优化:
- 路径压缩:在寻找代表元回溯的时候,将路径上的元素直接指向代表元。均摊复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)。
- 合并操作:将一个集合的代表元指向另一个集合的代表元,时间复杂度O(1)。
- 按秩合并(启发式合并):把深度小的集合(树)合并到深度较大的集合(树)上,这样也可以把查询复杂度降到 O ( l o g 2 n ) O(log_2n) O(log2n)。
- 实现代码:
//初始化
void init()
{
for(int i=1;i<=n;i++)
{
f[i]=i;
rank[i]=1;//按秩合并记录深度
}
return ;
}
//查询-路径压缩
int find(int x)
{
if(x==f[x])return x;
f[x]=findf(f[x]);//路径压缩
return f[x];
}
//合并-按秩合并
void merge(int x,int y)
{
x=find(x),y=find(y);
if(rank[x]<=rank[y])
f[x]=y;
else
f[y]=x;
if(rank[x]==rank[y]&&x!=y)//修正深度
rank[y]++;
}
02搜索优化
1. dfs优化
- 剪枝
剪枝,就是减小搜索树规模、尽早排除搜索树上不必要的分支的一种手段。
常见剪枝方法:- 优化搜索顺序:有时候,搜索的顺序不固定,而不同的搜索顺序产生的搜索树规模可能有很大差异。
- 排除等效冗余:如果搜索树中有几条分支的子树等效,那么只需要一条分支继续搜索。
- 可行性剪枝:及时检查当前状态能否到达目标状态,不能则立刻回溯。
- 最优性剪枝:有时候,如果当前解已经比当前的最优解差,那么无论如何都不能根性答案,直接回溯。
- 记忆化搜索:将每个状态的搜索结果记录下来,若搜到重复的状态就直接返回搜索结果。
- 迭代加深搜索
深度优先搜索的思路是“不撞南墙不回头”,它会不断执行一个分支,直到最深层。如果答案在较浅层次上,并且一开始选错了分支,就会浪费很多时间。于是,我们可以限制每次搜索的层数,每次只搜索若干层,若得不到答案就加深一层。
基本结构:
int p=1;//层次
while(!dfs(p))p++;//dfs函数返回是否得到答案
- 双向深搜
有的问题,我们不仅知道初态,还知道终态,我们就可以采用双向搜索——从初态和终态出发,各搜索得到一半深度的搜索树,在中间会合得到答案。这样可以大量减少搜索树规模。
2. bfs及其优化
- 广搜的特性——“单调性”、“两端性”
- 广搜在访问完第 i i i层节点后才会访问第 i + 1 i+1 i+1层节点。
- 广搜的任意时刻,只会至多存在两层节点,前一部分为第 i i i层的节点,后一部分为 i + 1 i+1 i+1层节点,并且第 i i i层节点总是在第 i + 1 i+1 i+1层节点。
- 双端队列bfs(0-1bfs)
这种广搜问题的权值为0或1,让我们求最小代价。
这时,我们可以使用双端队列来记录:对于边 u − v u-v u−v,若权值为0,则节点 u u u和 v v v属于同一层,将 v v v从前端压入队列;若权值为1,则节点 v v v在节点 u u u的下一层,将 v v v从后端压入队列。这种方法很好的利用了广搜的特性。 - 双向广搜
和双向深搜类似,我们可以从初态和终态分别开始,轮流搜索一层节点,当某个节点在两边都搜到时,就说明连成了一条搜索路径,合并得到答案。
03动态规划基础
- 基础知识:
- 通过已经得到的某一些状态来得到下一个状态的方法叫做第二数学归纳法,推理过程叫做**“动态规划”**,推理方法如图:
- 能使用动态规划解决的问题一般要具有的3个性质:
- 最优子结构:问题最优解所包含的子问题的解也是最优的。
- 无后效性:一个状态一旦确定,就不会影响到后续状态的决策。
- 有重叠子问题:子问题不是独立的,一个子问题可能在下一个阶段里多次使用(若不满足,动态规划没有优势)
- 一般思路:
状态、阶段、决策是构成动态规划算法的三要素。- 阶段:按问题的特征,把问题划分为若干个阶段,划分出的状态要有序。
- 状态:和搜索的状态近似,根据题目要求可得出不同状态,相同的题目也可能得出不同的状态,注意状态要满足无后效性。
- 决策(状态转移):寻找从一个状态转移到另一个状态的决策方法,得出递推式(状态转移方程)。
- 边界:一般很容易得出
- LIS(最长上升/不下降子序列):
- 状态: d p [ i ] dp[i] dp[i]表示前 i i i个数以最后一个数结尾的最长上升子序列长度。
- 决策/状态转移: d p [ i ] = m a x { d p [ j ] + 1 ∣ 1 ≤ j < i } dp[i]=max\{dp[j]+1|1\le j<i\} dp[i]=max{dp[j]+1∣1≤j<i}
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- LCS(最长公共子序列):
* 状态: d p [ i ] [ j ] dp[i][j] dp[i][j]表示 A A A的前 i i i个数和 B B B的前 j j j个数的最长公共子序列长度。
* 决策/状态转移:所有情况都符合 d p [ i ] [ j ] = m a x ( d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j ] ) dp[i][j]=max(dp[i][j-1],dp[i-1][j]) dp[i][j]=max(dp[i][j−1],dp[i−1][j]),若 A [ i ] = = B [ j ] A[i]==B[j] A[i]==B[j],则额外有 d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − 1 ] [ j − 1 ] + 1 ) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1) dp[i][j]=max(dp[i][j],dp[i−1][j−1]+1)。
* 时间复杂度: O ( n 2 ) O(n^2) O(n2) - 背包问题:
- 01背包:
0/1背包问题的模型如下:
给定 n n n个物品,其中第 i i i个物品的体积为 v [ i ] v[i] v[i],价值为 w [ i ] w[i] w[i]。有一容积为 m m m的背包,要求选择一些物品放入背包,使得物品总体积不超过 m m m的前提下,物品的价值总和最大。- 阶段:以已处理的物品件数,即前 i i i个物品作为阶段。
- 状态: d p [ i ] [ j ] dp[i][j] dp[i][j]表示只会使用前 i i i件物品和 j j j的体积可放入的最大价值。
- 决策/状态转移: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−v[i]]+w[i]),其中后一个状态仅当 j − v [ i ] ≥ 0 j-v[i]\ge0 j−v[i]≥0时可以转移到当前状态。
- 空间优化:
将 d p dp dp压缩为一维 d p [ j ] dp[j] dp[j],每次从大到小遍历 j j j才能保证数组取值正确。 - 多重背包:
在01背包的基础基础上添加了每个物品的使用次数 c [ i ] c[i] c[i]。- 思路:把每个物品的 c [ i ] c[i] c[i]件分成 c [ i ] c[i] c[i]个物品,然后进行01背包。
- 优化:可以利用二进制划分,将 c [ i ] c[i] c[i]件物品划分为 1 , 2 , 4 , . . . , 2 i , r 1,2,4,...,2^i,r 1,2,4,...,2i,r这些件数的物品,其中 2 i 2^i 2i是小于 c [ i ] c[i] c[i]的最大二的整数次方, r r r是剩下的件数。
- 完全背包:
在01背包的基础基础上不限制每个物品的使用次数。- 思路1:可以把一个物品拆分为 ⌊ m / v [ i ] ⌋ \lfloor m/v[i]\rfloor ⌊m/v[i]⌋个物品,进行多重背包。
- 思路2:可以把01背包的状态转移方程改为在当前阶段进行更新: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v [ i ] ] + w [ i ] ) dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]) dp[i][j]=max(dp[i−1][j],dp[i][j−v[i]]+w[i])
- 分组背包:
把n个物品变成了n组物品,然后每组物品中只能选一个。- 思路:跟01背包差不多,只是用“物品组数”作为阶段,并且多了一层循环用来枚举第 i i i组中的每个物品,选取其中一个物品作为当前的 d p dp dp值。
- 其他背包:
- 混合背包是把01、多重、完全三种背包混合起来,把01、多重背包拆成次数,完全背包可以单独考虑,也可以拆成次数。
- 二维费用背包是把01背包的限制条件变成了2个,和01背包的处理手法几乎一样。
- 01背包:
感谢阅读!
内容如有错误,请练习我,谢谢!