Neo's Blog

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

0%

求解一个区间的和乘以这个区间最小值的最大值

解题思路:单调栈 + 前缀数组

i到j的区间和,通过前缀数组,比较容易在O(1)的复杂度内取得。

对每一个元素,取得左边第一个比他小的元素,作为j;从后往前遍历,递增栈;

对每一个元素,取得右边第一个比他小的元素,作为i;从前往后遍历,递增栈;

整体时间复杂度$O(n)$

关于队列一共有两类问题

  1. 在一定条件下,实现某种具有某种特性的队列
  2. 借助队列的某些特性去巧妙的解决某一类问题。

队列的特点:先进先出

队列的用途:

  1. 单调队列解决滑动窗口最值问题
  2. 树或者图的BFS
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

//队列 —— 模板题 AcWing 829. 模拟队列
1. 普通队列:
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh <= tt) //如果hh <= tt, 则队列不空。
{

}

//2. 循环队列
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh != tt) //只要两者不相等,队列就不空。
{

}

实现队列

借助两个栈来实现一个队列

滑动窗口最值问题

利用一种特殊的队列(单调队列)来巧妙的解决滑动窗口最值问题。

请用栈实现一个队列,支持如下四种操作:

push(x) – 将元素x插到队尾;
pop() – 将队首的元素弹出,并返回该元素;
peek() – 返回队首元素;
empty() – 返回队列是否为空;
注意:

你只能使用栈的标准操作:push to top,peek/pop from top, size 和 is empty;
如果你选择的编程语言没有栈的标准库,你可以使用list或者deque等模拟栈的操作;
输入数据保证合法,例如,在队列为空时,不会进行pop或者peek等操作;
样例
MyQueue queue = new MyQueue();

queue.push(1);
queue.push(2);
queue.peek(); // returns 1
queue.pop(); // returns 1
queue.empty(); // returns false

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
class MyQueue {
public:
stack<int> is;
stack<int> os;

/** Initialize your data structure here. */
MyQueue() {

}

/** Push element x to the back of queue. */
void push(int x) {
is.push(x);
}

/** Removes the element from in front of queue and returns that element. */
int pop() {
if (os.empty()) {
while (!is.empty()) {
os.push(is.top());
is.pop();
}
}
int r = os.top();
os.pop();
return r;
}

/** Get the front element. */
int peek() {
if (os.empty()) {
while (!is.empty()) {
os.push(is.top());
is.pop();
}
}
return os.top();
}

/** Returns whether the queue is empty. */
bool empty() {
return is.empty() && os.empty();
}
};

/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* bool param_4 = obj.empty();
*/

1
2
3
4
5
6
7
8
9
10
//单调队列 —— 模板题 AcWing 154. 滑动窗口
//常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
//新的元素入队
q[ ++ tt] = i;
}

什么是LRU缓存,怎么设计的LRU缓存。
时间复杂度:$O(1)$

  1. Hash + 双向链表
  2. 哨兵节点来简化判断
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

struct ListNode {
int val;
ListNode* next;
ListNode* pre;
}


class LRUCache {
private:
unordered_map<int, ListNode*> hash;
ListNode* head;
ListNode* tail;
int n;

LRUCache(int nn) {
n = nn;
head = new ListNode();
tail = new ListNode();
head->next = tail;
tail->pre = head;
}

void put(int k, int v) {
ListNode* r = get2(k);
if (r != NULL) return;

ListNode* node = NULL;
if (hash.size() > n) {
//链表上移除
auto node = tail->pre;
node->next->pre = node->pre;
node->pre->next = node->next;
//hash移除
hash.erase(node->k);
}
else {
node = new ListNode();
}

node.val = v;
//将node放在头部
node->next = head->next;
node->pre = head;

head->next->pre = node;
head->next = node;

hash[k] = node;

}

ListNode* get2(int k) {
auto it = hash.find(k);
if it == hash.end() {
return NULL;
}

auto node = *it;

//移除
node->next->pre = node->pre;
node->pre->next = node->next;

node->next = head->next;
node->pre = head;

head->next->pre = node;
head->next = node;
}

int get(int k) {
ListNode* r = get2(k);
if (r == NULL) return -1;
else r->val;
}
};

二叉堆,包括最大堆、最小堆

最大特点:在$O(1)$时间内找到最小(最大)元素

堆的常见应用:

  1. 堆排序
  2. 用两个堆(一个最大堆,一个最小堆)来维护/查询第K大/小的操作
  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
堆 —— 模板题 AcWing 838. 堆排序, AcWing 839. 模拟堆
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}

void down(int u)
{
int t = u;
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}

void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}

// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);

给定一个长度为 n+1 的数组nums,数组中所有的数均在 1∼n 的范围内,其中 n≥1。

请找出数组中任意一个重复的数,但不能修改输入的数组。

样例
给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。

返回 2 或 3。

思考题:如果只能使用 O(1) 的额外空间,该怎么做呢?

思考: 把肯定不符合条件的一半给忽略掉;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int l = 1, r = nums.size() - 1;
while (l < r) {
int mid = (l + r) >> 1; //[l, mid], [mid + 1, r]
int s = 0;
//注意这里是遍历了所有的元素
for (int i = 0; i < nums.size(); ++i) s += (nums[i] >= l && nums[i] <= mid);
if (s > (mid - l + 1)) r = mid;
else l = mid + 1; //数量不够,说明这个区间内的某个数肯定被替换掉了,所以肯定在另外一边。
}

return r;
}
};

给定一个长度为 n 的整数数组 nums,数组中所有的数字都在 0∼n−1 的范围内。

数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。

请找出数组中任意一个重复的数字。

注意:如果某些数字不在 0∼n−1 的范围内,或数组中不包含重复数字,则返回 -1;

样例
给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。

返回 2 或 3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <algorithm>

class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; ++i)
if (nums[i] < 0 || nums[i] > n - 1) return -1;

for (int i = 0; i < n; ++i) {
while (i != nums[i]) {
int x = nums[i];
if (nums[x] == x) return x; //如果坑位已经被占了,说明重复了。
else swap(nums[x], nums[i]);
}
}
x
return -1;
}
};

根据1到m生成1到n随机数

问题1: 给定一个函数rand()能产生1到m之间的等概率随机数,产生1到n之间等概率的随机数?

1
2
3
4
5
6
7
8
9
int rand10()
{
int x;
do
{
x = (rand7()-1)*7+rand7();
}while(x > 40) //【1, 40】的范围
return (x-1) / 4+1; //分为10段,0~9 + 1 =》 1 ~ 10
}

考虑如果rand()返回的是浮点呢?

这种情况下可以简化为,如何把[a,b]线段上的点等概率的映射到[c,d]

另外, 为啥不能考虑rand() * rand()呢?

因为不等概率,76与67的计算结果都是42,这样子就使得出现42的概率是1/49 + 1/49;出现1的概率是1/49。

参考:

https://www.cnblogs.com/double-win/archive/2014/04/07/3650314.html

https://my.oschina.net/u/4401597/blog/4038277

p & 1 -p 生成等概率

问题2: 有一个随机生成器randA(),以p的概率返回0,1-p的概率返回1,利用这个randA()构造randB(),使randB()等概率的返回0和1,即0.5的概率返回0,0.5的概率返回1。

分析比较简单: 出现00的概率是p^2; 出现10的概率是p(1-p),出现01的概率是p(1-p); 出现11的概率是(1-p)^2;

以上,只要丢弃掉00,01的结果; 10与01的出现概率就是相等的;也就各为50%。

1
2
3
4
5
6
7
8
9
10
int randB()
{
int x1, x2;
do
{
x1 = randA();
x2 = randA();
}while(x1+x2 != 1)
return x1;
}

已知一随机发生器,产生0的概率是p,产生1的概率是1-p,
现在要你构造一个发生器,
使得它构造0和1的概率均为 1/2;
构造一个发生器,使得它构造1、2、3 的概率均为 1/3; …,
构造一个发生器,使得它构造 1、2、3、…n 的概率均为1/n,要求复杂度最低。

思路:
由于需要产生1/2,而用1位0,或1位1无法产生等概率,
因此,考虑将随机数扩展成2位:
00 pp
01 p
(1-p)
10 (1-p)p
11 (1-p)
(1-p)
有上述分析知道,01和10是等概率的,因此我们只需要产生01和10就行了。
于是可以,遇到00和11就丢弃,只记录01和10。可以令,01表示0,10表示1,则等概率1/2产生0和1了。
对于n=2,一次性生成两个数字,认为01表示0,10表示1,
其它情况放弃,它们的概率都是p(1-p);
对于n=3,一次性生成三个数字,认为001表示0,010表示1,100表示2,
其它情况放弃,它们的概率都是p
p(1-p);
对于n=4,一次性生成是个数字,认为0001表示0,0010表示1,0100表示2,1000表示3,
其它情况放弃,它们的概率都是p
pp(1-p);
5为例,此时我们取x=2,因为C(2x,x)=C(4,2)=6是比5大的最小的x,
此时我们就是一次性生成4位二进制,把1出现个数不是2的都丢弃,
这时候剩下六个:0011,0101,0110,1001,1010,1100,
取最小的5个,即丢弃1100,那么我们对于前5个分别编号1到5,
这时候他们的概率都是pp(1-p)*(1-p)相等了。
关键是找那个最小的x,使得C(2x,x)>=n这样能提升查找效率。
因为C(n,i)最大是在i接近n/2的地方取得,此时我有更大比率的序列用于生成,
换句话说被抛掉的更少了,这样做是为了避免大量生成了丢弃序列而使得生成速率减慢,
实际上我之所以将x取定是为了让我取得的序列生成的概率互相相等,
比如C(2x,x)的概率就是[p(1-p)]^x,
互等的样例空间内保证了对应的每个值取得的样例等概率。

Shuffle算法

一个从1到n的序列,随机打乱,保证每个数出现在任意一个位置的概率相同。

基于交换,将a[i]与arr[rand(i, n - 1)]随机交换;

1
2
3
4
5
6
7
8
9
10
11
// 得到一个在闭区间 [min, max] 内的随机整数
int randInt(int min, int max);

void shuffle(int[] arr) {
int n = arr.length();
for (int i = 0 ; i < n; i++) {
// 从 i 到最后随机选一个元素
int rand = randInt(i, n - 1);
swap(arr[i], arr[rand]);
}
}

N个数随机选M

问题3: 程序的输入包含两个整数m和n,其中$m \lt n $。输出是0~n-1范围内m个随机整数的有序列表,不允许重复。从概率的角度来说,我们希望得到没有重复的选择,其中每个选择出现的概率相等。

选出这个序列的概率是:$m/n (m-1) / (n-1) … * (1)/(n-m)$

1
2
3
4
5
6
7
8
9
10
11
12
void generate(int m,int n)
{
int t = m;
for(int i = 0; i < n; i++)
if(Rand(0,n-1-i) < t) //即以t/(n-i)的概率执行下面的语句
{
printf("%d\n",i); //这里打印的是i,一直在增长;所以不会重复!!!
t--;
}
}
//随着算法继续,i越大,rand(0,n-1-i)得到的数值越小,就容易被选中,
//如果i已经等于n-1-t了,则后面的必然全不都被选中。

蓄水池采样算法

问题4: 如何数据流中随机选出N个元素

蓄水池采样算法,具体步骤如下:

(1)先把N个坑位填上

(2)对于后面新来的第i元素,做如下概率处理:令X=rand(0, i), i > N

A. 如果X在[0, N]之间,则用swap(X, i)

B. 否则啥也不做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i = 0;
vector<int> res;
while (cin >> Elem) {
i++;
if (i < N) {
res.push(Elem);
continue;
}

X = rand(0, i);
if (X < N) {
swap(res[X], res[i]);
}
}

深入一下,分布式蓄水池算法: https://www.jianshu.com/p/7a9ea6ece2af

Probability simulation

You are given a fair coin. Can you design a simple game using the fair coin so that your
probability of winning is p, 0 < p < 1

把p表示为2进制小数;例如0.100011100110101

开始抛硬币,正面1; 反面0,计作s[i]

如果第i次抛出1,而p[i] = 0, 则win;
如果第i次抛出0,而p[i] = 1, 则lose;
如果s[i] == p[i],则继续新的循环

概率分布

男孩女孩问题

如果一个地区所有人都生了女孩就继续生,直到生出男孩立马停下,这个地区会趋于什么样的男女比例?

1:1 ,几何分布

https://www.zhihu.com/question/355605125

一根绳子分三段,能够合成三角形的概率

线性规划 + 几何概率, 1/4

如果在高速公路上30分钟内到一辆车开过的几率是0.95,那么在10分钟内看到一辆车开过的几率是多

算法题2:给定k个数组,每个数组都是有序的,且每个数组最大值-最小值<1000,1 < k<1000,求所有数的中位数。

解答思路:

定义一个结构体 NumCount {
int number;
int cnt;
}

预处理所有数据,将原来的数据用NumCount nums[k][1000]来保存。

用一个大顶堆来维护K路的最小值。

时间复杂度:$O(1000 * k)$