Neo's Blog

不抽象就无法深入思考
不还原就看不到本来面目!

0%

支持最大值的队列 - 题目描述

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

1 <= push_back,pop_front,max_value的总操作数 <= 10000

1 <= value <= 10^5

支持最大值的队列-实现思路

首先,默写下普通队列的模版:

1
2
3
4
5
6
7
8
9
int q[N];
int hh = 0;
int tt = -1;
//push
q[++tt] = x;
//pop
q[hh++];
//is empty
hh > tt;

其次,默写下单调队列的模版:

1
2
3
4
5
for (int i = 0; i < n; ++i) {
while (tt >= hh && check_out(q[hh])) hh++; //判断是否滑出窗口
while (tt >= hh && check(q[tt])) tt--;
q[++tt] = i;
}

最后为什么会想到单调队列?

一句话:如果一个后入队的元素比我还大,那么我永无出头之日!

支持最大值的队列-代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
 class MaxQueue {
public:
MaxQueue() {
memset(q, 0, sizeof(q));
hh = 0;
tt = -1;
}

int max_value() {
if (dq.size()) return q[dq.front()];
else return -1;
}

void push_back(int value) {
q[++tt] = value;
//if (dq.size() && value >= q[dq.front()]) dq.clear();
while (dq.size() && value > q[dq.back()]) dq.pop_back();
dq.push_back(tt);
}

int pop_front() {
if (tt < hh) return -1;

//当前的hh
if (dq.size() && dq.front() == hh) {
dq.pop_front();
}

return q[hh++];
}

private:
int q[10010];
int hh; //队头
int tt; //队尾
std::deque<int> dq;
};

/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue* obj = new MaxQueue();
* int param_1 = obj->max_value();
* obj->push_back(value);
* int param_3 = obj->pop_front();
*/

什么是熵?(Entropy)

熵,代表了一个系统的混乱程度。一个系统越混乱,其熵越大。一个封闭系统的熵随着时间推移,会一直变大,且不可逆。

熵增定律

什么是信息熵?(Information Entropy)

一条信息的信息量大小和它的不确定性有直接的关系。不确定性越大,其信息量越大。我们用信息熵被用来计算信息量,其计算公式如下:
H(x) = -∑p(xi)log(p(xi)) (i=1,2,..n) (其中p(x)是x事件出现的概率),信息熵的单位是bit。

获得了新的信息后,不确定性减少,信息熵降低。

问题的解空间与信息熵的关系

这里提一个很重要的概念:问题的解空间。什么是问题的解空间呢?所有可能成为问题最优解的解所构成的空间。问题的解空间越大,不确定性就越大,问题就“越混乱”,熵也就越高。

计算机求解问题的过程是怎样的呢?计算机算法在执行的过程中,获取更多信息,将要解决的问题的信息熵减小,直到减到0,该问题就被解决了。

每一个问题都有一个固有的初始信息熵,这个初始信息熵的大小也就决定了这个问题的复杂程度。例如,N皇后这个问题就比“从N个数中找到最大值”问题更难,因为前者的解空间更大,有更多的可能性。

问题的信息熵与其解空间大小的关系

不同的算法为什么执行时间差别那么大?

算法执行时间与信息熵的关系

同一个问题,有不同的解法,也就会产生不同的解空间。

一开始的时候,要解决的问题信息熵很大,不确定性就越大。从信息熵的角度来看,就是把这个问题的熵将为0的过程。
什么是问题的求解呢?
计算机算法是如何获取新的信息的呢? 信息获取的越快,
举个例子,从N个数中找到某一个数这个问题。一开始这个问题有N种可能性,
搜索空间就是你算法的自变量构成的空间,而求解问题的解空间是你问题的解构成的空间。所以当你算法的自变量范围和求解问题的解范围相同时,这两个空间就是相同的。但是在很多时候,它们并不相同。这是因为求解的问题往往存在约束。大多数情况下,我们在处理约束时,都是采用惩罚函数的方式。这相当于我们把求解问题对解的约束放在了目标函数上。由此,变相地放大了自变量的空间,也就是算法的搜索空间。

让我们结合几个例子来

常见问题的解空间有多大

子集问题
$2^k$

组合问题

排列问题
n!

参考文章:
https://stonema.github.io/2018/03/27/大话交叉熵/

一条信息的信息量大小和它的不确定性有直接的关系。比如说,我们要搞清楚一件非常非常不确定的事,或是我们一无所知的事情,就需要了解大量的信息。相反,如果我们对某件事已经有了较多的了解,我们不需要太多的信息就能把它搞清楚。所以,从这个角度,我们可以认为,信息量的度量就等于不确定性的多少。(摘自数学之美)

香浓指出的信息熵的计算公式如下(本文的对数一律以2为准)

H(x) = -∑p(xi)log(p(xi)) (i=1,2,..n) (其中p(x)是x事件出现的概率)单位为bit
算法时间复杂度,与信息熵

数学之美里是用赛后怎么知道32个球队里谁是冠军来讲解了这个信息熵的概念。

当概率相等时,每次询问用折半查找的原理(如“冠军队伍在1-16吗?”)可以减少一半的队伍,这样就需要5次就能知道结果了。这里就是log32 = 5

而使用信息熵计算信息量,的确也是5。但是为什么信息熵这个公式会代表信息量呢

按我的理解,在等概率事件里,1/p(x) 代表那一次所有可能出现的量、在球队问题里,就是32种可能性。

而等概率事件里,因∑p(xi) = 1,所以信息熵可以看成

信息熵H(x)= -∑p(xi)log(p(xi)) (i=1,2,..n) = -log(p(i)) = -(- log(1/p(x)))= log(1/p(x))

也就是说等概率事件里的信息量可以看成

H(x)= log(所有可能性)

为了加深对信息量的定义的理解,再回到上述32个球队的问题,我们已经知道他的信息量是5Bit

问过一次之后,我们可以知道冠军在哪16个队伍中,也就是说我们获得了1bit的信息后不确定性减少,等于信息熵变成了log16 = 4bit =5bit -1bit

而最大熵模型呢?它的原理就是保留全部的不确定性,将风险降到最少。

最大熵原理指出,当我们需要对一个随机事件的概率分布进行预测时,我们的预测应当满足全部已知的条件,而对未知的情况不要做任何主观假设。(不做主观假设这点很重要。)在这种情况下,概率分布最均匀,预测的风险最小。因为这时概率分布的信息熵最大,所以人们称这种模型叫“最大熵模型”。我们常说,“不要把所有的鸡蛋放在一个篮子里”,其实就是最大熵原理的一个朴素的说法,因为当我们遇到不确定性时,就要保留各种可能性。

也就是说发现不确定信息的时候,不要对不确定的产物任何主观假设使他们的概率分布均匀,则能获得最客观的结果。而这时风险会最小,我们就可以用这个结果来进行最客观的决策。数学上来说貌似就是最优下界吧。这种策略的本质可以概括成“让未知世界无机可乘”。它是没有“弱点的”,答案的任何一个分支都是等概率的。反之,一旦某个分支蕴含的可能性更多,当情况落到那个分支上的时候你就郁闷了。二分搜索为什么好,就是因为它每次都将可能性排除一半并且无论如何都能排除一半(它是最糟情况下表现最好的)。(摘自mindhacks快排为什么那么快)

我再用算法的时间复杂度说明一下最大熵原理吧,用几个主流的算法对n个数据进行排序时间复杂度基本上都是从O(nlogn)到O(n2)。而一般情况下为什么O(nlogn)最优呢,因为n个数据的先后顺序是随机的,我们可以看做不确定性相等,则可以用最大熵原理获得最优(最稳定)结果。则信息熵则为

H(x)= log(所有可能性)= log(n!) 而n—>00 则log(n!)近似于lognn= nlogn

假设我们每次能获得1bit数据,就至少需要获得(nlogn)bit数据才能取消信息的不确定性,也就是要比较nlogn次。但因为各种排序算法策略不同,我们不可能每次都能获得1bit数据,所以按照信息熵的定义这是理论上最优的结果。而最佳的排序算法就是要每次获得1bit数据,越接近于1则越有效。

而TopLanguage那个帖子里也说了,虽然快排和堆排序两个是都是时间复杂度O(nlogn)的算法,但是快速排序一般都会比堆排序快,就是因为堆排序每次获取的平均信息量比快排来的低。

而上面,我们根本没提到具体算法,就算到了最优的时间复杂度。在实际生活中很多时候我们虽然不会想到具体的策略,但我们至少可以知道极限在哪里,可以知道还有没有提高余地。任何排序和猜数字的算法可以理解为通过获得信息量去消减原来的熵。(这句摘自eric的话)

什么叫状态空间树?

就是问题的解空间树,分为子集树和排列树

当所给的问题是从n个元素的集合S中找到S满足某种性质的子集时,相应的解空间树称为子集树。

当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。

子集树和排列树的不同是每一步的选择策略不同。子集树每一步对应的是对应的元素的选择或不选择,排列树每一步对应的是剩下的元素选择其中一个。一个的可搜索的解为2**n,一个为n!,因此,一个高效的回溯法算法必须依赖于剪枝函数来避免无效搜索。

面对许多实际问题时,需要求解满足特定条件的全部解或最优解,如著名的N皇后问题和旅行售货员问题。
此类问题,一般没有特定的计算规则用于解题,通常我们采用试探性的方法,在包含问题所有可能解的解空间树中,将所有可能的结果搜索一遍,从而获得我们期望的那一个解,或者是那一些解,一般就是满足一定条件的最优解,或是全部解。那么这里用到的解空间树是什么呢?

解空间树:
依据待解问题的特性,用树结构表示问题的解结构,用叶子表示所有问题所有可能解的一棵树。

解空间树的建立:
就是将问题求解的一系列判断决策过程及各种可能的结果用树型结构呈现。
事实上,我们解题的过程是一个不断判断决策的过程,我们把每一步判断决策的过程对应于解空间树的一个分支结点,而各种可能的不同结果,则对应得到结点的每一个孩子及各棵子树,而一系列判断决策的解题过程,就对应着解空间树的生长过程;而问题最终所有可能的解,都会呈现在这棵解空间树的叶子上。

让我们先思考下我们之前做过的算法题都有哪些种类?

解空间树T

回溯法以深度优先的方式搜索解空间树T,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树T。它们在问题的解空间树T上搜索的方法不同,适合解决的问题也就不同。一般情况下,回溯法的求解目标是找出T中满足约束条件的所有解的方案,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。相对而言,分支限界算法的解空间比回溯法大得多,因此当内存容量有限时,回溯法成功的可能性更大。

发现当考虑了所有的操作时,还要对操作所得状态进行判断,是否已出现过,来避免重复搜索。这道题我纠结了好久,就是因为我想通过限制操作来避免出现重复状态,而出现重复状态的情况有很多,很难考虑全面。其实判断状态是否重复我之前也都做过,就是在搜索迷宫的过程中,操作就是移动的方向,而状态就是位置,为了避免重复搜索就在地图上做标记,当时这样做的时候觉得顺理成章,不过在这题里,却并没有能直接这样做。
总结下,在搜索整个解空间时,首先是考虑所有的 操作,然后通过保存已出现 状态,来防止重复搜索。如果很容易通过对操作进行限制来防止状态的重复出现,或是保存状态所需的内存空间过大的话,那就不宜保存状态。

a computational problem is a problem that a computer might be able to solve or a question that a computer may be able to answer.
A computational problem can be viewed as an infinite collection of instances together with a, possibly empty, set of solutions for every instance. For example, in the factoring problem, the instances are the integers n, and solutions are prime numbers p that describe nontrivial prime factors of n.

A decision problem is a computational problem where the answer for every instance is either yes or no.

In a search problem, the answers can be arbitrary strings. For example, factoring is a search problem where the instances are (string representations of) positive integers and the solutions are (string representations of) collections of primes.

决策问题,给定一个正整数n,确定n是否是素数。

搜索问题,找到一个正整数的所有非朴素因子。

A counting problem asks for the number of solutions to a given search problem. For example, a counting problem associated with factoring is

“Given a positive integer n, count the number of nontrivial prime factors of n.”

An optimization problem asks for finding a “best possible” solution among the set of all possible solutions to a search problem.

求这n个实数在数轴上相邻2个数之间的最大差值,

In a function problem a single output (of a total function) is expected for every input, but the output is more complex than that of a decision problem, that is, it isn’t just “yes” or “no”. One of the most famous examples is the traveling salesman problem:

“Given a list of cities and the distances between each pair of cities, find the shortest possible route that visits each city exactly once and returns to the origin city.”
It is an NP-hard problem in combinatorial optimization, important in operations research and theoretical computer science.

function problem

我们要解决一个

当不确定性降到了0,问题就解决了。

信息:

熵:

高熵特征,低熵特征

熵增定律

多做一步的目的是为了减少更多的不确定性。

柱状图中最大的矩形 - 题目描述

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。

柱状图中最大的矩形-一从暴力开始

首先,思考一下问题的解空间有多大(也就是说有多少种选择),显然一个开始位置、一个结束位置,所以我们可以得到暴力解法的思路:
(1)双层循环确定开始结束位置,也就是矩阵的长
(2)从开始到结束扫描一遍,找到最小值,也就是矩阵的宽
(3)长乘宽得到面积,与答案比较,并更新答案

柱状图中最大的矩形-一暴力扫描代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
int largestRectangleArea(vector<int>& heights) {
int n = height.size();
int res = 0;
for (int i = 0; i < n; ++i) {
for (int j = i; j < n; j++) {
int minv = INF;
for (int k = i; k <= j; ++k) {
minv = min(minv, height[k]);
}
res = max(res, (i - j + 1) * minv);
}
}
return res;
}

class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
vector<int> s(heights.size() + 2);
vector<int> w(heights.size() + 2);
int p = 0;

heights.push_back(0);
int res = 0;
for (int i = 0; i < heights.size(); ++i) {
if (heights[i] > s[p]) {
s[++p] = heights[i];
w[p] = 1;
}
else {
int width= 0;
while (s[p] > heights[i]) {
width += w[p];
res = max(res, width * s[p]);
p--;
}
s[++p] = heights[i];
w[p] = width + 1;
}
}
return res;
}
};vf

public class Solution {
public int calculateArea(int[] heights, int start, int end) {
if (start > end)
return 0;
int minindex = start;
for (int i = start; i <= end; i++)
if (heights[minindex] > heights[i])
minindex = i;
return Math.max(heights[minindex] * (end - start + 1), Math.max(calculateArea(heights, start, minindex - 1), calculateArea(heights, minindex + 1, end)));
}
public int largestRectangleArea(int[] heights) {
return calculateArea(heights, 0, heights.length - 1);
}
}

int largestRectangleArea(vector<int>& heights) {
int n = height.size();
int res = 0;
for (int i = 0; i < n; ++i) {
int minv = height[i];
for (int j = i; j < n; j++) { //确保j从小变大
int minv = INF;
//如果i看作一个定值,下面这个循环,其实每次都做了相同的工作,重复扫描[i, j]
//所以可以做累计处理
//for (int k = i; k <= j; ++k) {
// minv = min(minv, height[k]);
//}
minv = min(minv, height[j]);
res = max(res, (i - j + 1) * minv);
}
}
return res;
}

时间复杂度:$O(n^3)$ => $O(n^2)$
空间复杂度:$O(1)$

柱状图中最大的矩形-进一步思考

还有一种解法,一种更有智慧的解法,不像前面的暴力解法那么粗暴,让我们先考虑下哪些肯定不会成为答案。
如果一个格子往两边延伸,一直延伸到有比它更低的格子为止。
柱状图中最大的矩形-分治法
如果备选答案覆盖了矩形A,那么备选答案的宽度一定会延伸到绿色矩形这里。

假设有N个像上面的这种备选答案,那么最终答案一定是出自这N个备选答案。

为什么我们这么考虑呢? 因为这样子考虑的话,有利于我们分解子问题。

柱状图中最大的矩形-空间换时间版

1
2
3
4
5
6
7
8
9
10
11
12
int dfs(vector<int>& height, int l, int r) {
int p = find_min(height, l, r);

int left_lower = 0//往左查找,直到找到比height[p]更小的元素
int right_lower = 0//往右查找,直到找到比height[p]更小的元素

return max(dfs(l, p - 1), dfs(p + 1, r), height[p] * (right_lower - left_lower);
}

int largestRectangleArea(vector<int>& height) {
return dfs(height, 0, height.size() - 1);
}

递推公式:$T(n) = 2*T(n/2) + O(n)$
时间复杂度:$O(nlgn)$
空间复杂度:$O(1)$

单调栈解法

找右边,比我小的(利用递增栈);找到了,就可以计算候选了。

左边的都比我小,left确定了;

新来的,比我小,right也确定了; 所以面积就确定了!

给定一个数组height,长度为n,每个数代表坐标轴中的一个点的高度,height[i]是在第i点的高度,请问,从中选2个高度与x轴组成的容器最多能容纳多少水
1.你不能倾斜容器
2.当n小于2时,视为不能形成容器,请返回0
3.数据保证能容纳最多的水不会超过整形范围,即不会超过2^31-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int maxArea(vector<int>& height) {
int res = 0;
int i = 0, j = height.size() - 1;
int lmax = 0, rmax = 0;
while (i < j) {
lmax = max(lmax, height[i]);
rmax = max(rmax, height[j]);
res = max(res, min(lmax, rmax) * (j - i));
if (lmax < rmax) {
i++;
} else {
j--;
}
}

return res;
}
};

接雨水问题 - 题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6

接雨水问题-一开始的思路

经典解法包括:
(1)单调栈,左右遇到的最大值
(2)双指针

首先,也是最重要的,先把题目搞清楚(好多时候解不出题是由于题目没有完全理解、没有理解完全或者完全没有理解)。
按照题意,一个格子能够接的水,取决于该格子向前、向后遇到的最大值,两个最大值取最小值;而我之前曾经按照往前、往后遇到的比该格子高的格子来算了。

其次,让我们考虑最暴力的做法是什么?暴力扫描。

接雨水问题-一暴力扫描代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int trap(vector<int>& height) {
int n = height.size();
int res = 0;
for (int i = 1; i < n; ++i) {

//往左找到最大值
int lmax = 0;
for (int j = 0; j < i; ++j) {
lmax = max(lmax, height[j]);
}

//往右找到最大值
int rmax = 0;
for (int j = i + 1; j < i; ++j) {
rmax = max(rmax, height[j]);
}

res += min(rmax, lmax) - height[i];

}
}

时间复杂度:$O(n^2)$
空间复杂度:$O(1)$

接雨水问题-进一步思考

观察上面的代码,有很多重复计算,例如计算i左边的最大值时,扫描范围是[0, i - 1];当计算i+1左边的最大值时,扫描范围是[0, i],显然是完全包含[0, i - 1]的,
对于重复计算,我们继续采用空间换时间的套路,用数组lmax[i]来存储[0, i - 1]的最大值;这样子存储之后,如果再计算lmax[i + 1]只要拿lmax[i]跟height[i + 1]比较即可。

接雨水问题-空间换时间版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int trap(vector<int>& height) {
int n = height.size();
int res = 0;
int lmax[N], rmax[N];
//正序遍历
for (int i = 1; i < n; ++i) {
lmax[i] = max(lmax[i - 1], height[i - 1]);
}

//倒序遍历
for (int i = n; i >= 0; --i) {
rmax[i] = max(rmax[i + 1], height[i + 1]);
}

for (int i = 1; i < n; ++i) {
res += min(lmax[i], rmax[i]) - height[i];
}
}

时间复杂度:$O(n)$
空间复杂度:$O(n)$

接雨水问题-能不能把空间复杂度优化掉?

其实,上面的时间复杂度已经是最优了,有没有可能把空间也优化掉呢?我们注意到lmax[i]仅跟lmin[i-1]有关系,这个对于我们做空间压缩是很好的;
剩下的就是,我们能不能把lmax,rmax立即用上,而不需要存起来呢?答案是可以的。我们可以把上面的循环写在一起,用i、j两个指针分别指向左右两个端点,在遍历的过程中,直接把前面出的lmax, rmax立即用上。

接雨水问题-双指针优化版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int trap(vector<int>& height) {
int n = height.size();
int res = 0;
int lmax, rmax;
int i = 1, j = n - 2;

while (i < j) {
lmax = max(lmax, height[i - 1]);
rmax = max(rmax, height[j + 1]);
if (lmax < rmax) {
res += lmax - height[i];
i++;
}
else {
res += rmax - height[j];
j--;
}
}
return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

class Solution {
public:
int trap(vector<int>& height) {
int lmax = 0, rmax = 0;
int l = 0, r = height.size() - 1;
int sum = 0;

while (l <= r) {
for (; l <= r && (lmax = max(height[l], lmax)) <= rmax; ++l) {
sum += lmax - height[l];
}
for (; r >= l && (rmax = max(height[r], rmax)) <= lmax; r--) {
sum += rmax - height[r];
}
}

return sum;
}
};

时间复杂度:$O(n)$
空间复杂度:$O(1)$

单调栈解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

class Solution {
public:
/**
* max water
* @param arr int整型vector the array
* @return long长整型
*/
long long maxWater(vector<int>& arr) {
stack<int> s;
s.push(-1);
int res = 0;
for (int i = 0; i < arr.size(); ++i) {
while (s.top() != -1 && arr[s.top()] < arr[i]) {
int cur = s.top(); //当前计算的格子
s.pop();
int left = s.top();
if (left >= 0) {
res += (min(arr[left], arr[i]) - arr[cur]) * (i - left - 1);
//cout << "i:" << i << ", cur:" << cur << ", res: " << res << endl;
}
}

s.push(i);
}

return res;
}
};

前缀和和差分,通过空间来换时间,一般用来优化时间复杂度,从$O(n^2)$到$O(n)$

//一维前缀和 —— 模板题 AcWing 795. 前缀和

1
2
3
4
int prefix_sum() {
S[i] = a[1] + a[2] + ... a[i];
a[l] + ... + a[r] = S[r] - S[l - 1];
}

//二维前缀和 —— 模板题 AcWing 796. 子矩阵的和

1
2
3
4
5
6
int 2d_preifx_sum()
{
S[i, j] = 第i行j列格子左上部分所有元素的和
//以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
}

//一维差分 —— 模板题 AcWing 797. 差分
给区间[l, r]中的每个数加上c:

1
B[l] += c, B[r + 1] -= c

//二维差分 —— 模板题 AcWing 798. 差分矩阵
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:

1
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
题目分类 题目名称 考察点 其他说明
前缀和系列 不使用除法的特殊累乘 前缀和、定序技巧

题目分类 题目名称 考察点 其他说明
最长无重复字串 滑动窗口、字符串类
1
2
3
4
5
6
7
8
9
//滑动窗口 模版
for (int i = 0, j = 0; i < n; i ++ )
{
//i是新进入窗口的元素
//[j,i]是窗口边缘,j是要滑出的下标,i是新进入的下标
while (j < i && check(i, j)) j ++ ; //j是可能要滑出窗口的元素

// 具体问题的逻辑
}

最长无重复字串 - 题目描述

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

假设字符串中只包含从’a’到’z’的字符。

样例
输入:”abcabc”

输出:3

最长无重复字串-实现思路

首先,默写下滑动窗口的模版:

1
2
3
4
5
6
7
8
9
int i = 0, j = 0, n;
while (i < n) {
//对新进入窗口的元素i进行处理

while (j < i && check(xx)) {
//对出窗口的元素j进行处理
j++;
}
}

其次,搞清楚滑动窗口是用来做什么的以及用什么数据结构来维护

很明显,我们需要使用滑动窗口来存储每一个字母的出现次数,并且在当前窗口内的字母不重复;数据结构的话,hashmap即可

最长无重复字串-代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int longestSubstringWithoutDuplication(string s) {
int i, j = 0, n =s.size();
unordered_map<char,int> counter;
int res = 0;
while (i < n) {
char c = s[i];
counter[c]++;
if (counter[c] == 1) { //新来了一个不重复元素,则窗口扩大了
res = max(res, i - j + 1)
}
i++;
while (j < i && counter[c] > 1) { //如果现在窗口已经不在符合条件,则一直往前移动left,直到窗口重新满足
counter[s[j]]--;
j++;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int maxLength(vector<int>& arr) {
unordered_map<int, int> counter;

int i = 0, j = 0, res = 0;
while (j < arr.size()) {
counter[arr[j]]++;
while (counter[arr[j]] > 1) {
counter[arr[i++]]--;
}

res = max(j - i + 1, res);
j++;
}

return res;
}
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

class Solution {
public:
unordered_map<char, int> pattern;

string minWindow(string src, string tt) {
//将目标串转换为方便检查处理的hashmap
for (auto x : tt) {
pattern[x]++;
}
int cnt = pattern.size();

string res;
for (int i = 0, j = 0, c = 0; i < src.size(); ++i) {
if (pattern[src[i]] == 1 ) c++;
pattern[src[i]]--; //将src[i]标记为缺少状态
//满足了,并且某一个字符是缺的【非pattern中的字符一定缺;并且一定会被补上】,则往前移动
while (c == cnt && pattern[src[j]] < 0) {
pattern[src[j++]]++;
}
if (c == cnt) {
if (res.empty() || res.size() > i - j + 1) res = src.substr(j, i - j + 1);
}
}
return res;
}
};