Fisher-Yates 洗牌算法

可以原地实现打乱数组。

1
2
3
4
5
6
7
vector<int> nums; int n=nums.size();
for(int i=0; i<n; i++) {
uniform_int_distribution<> dis(i, n-1); // range is [i, n-1]
int j=dis(gen);
swap(nums[i], nums[j]);
}
return nums;

该程序循环n次,每次从[i, n-1]选取一个数字,然后把它交换到第i位。

概率分析:
原数组中的任意一个数字,在第i次被选出从而放到第i个位置的概率是

1
2
3
4
P(0)=1/n
P(1)=(n-1/n)*(1/n-1)=1/n
...
P(i)= (n-1/n)*(n-2/n-1)*...*(1/n-i)=1/n

所以每个数字出现在每个位置的概率等同。

全排列中获取下一个排列的实现(c++ STL next_permutation)

维基百科中的步骤:

  1. Find the largest index k such that a[k] < a[k + 1]. If no such index exists, the permutation is the last permutation.
  2. Find the largest index l greater than k such that a[k] < a[l].
  3. Swap the value of a[k] with that of a[l].
  4. Reverse the sequence from a[k + 1] up to and including the final element a[n].

至于为什么这么做就可以得到下一个排列,可以看代码及注释。

实现代码:

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
class Solution {
public:
void nextPermutation(vector<int>& nums) {
if(nums.size()==1) return;
// find the largest index i which statisfies nums[i]<nums[i+1]
// 从后往前找到交换点i,交换点i以后的数全部都是降序的
// 因为i后面的数[i+1, n)已经无法变得更大,所以我们不得不让i位置变大一点来得到下一个序列
int i=nums.size()-2;
while(i>=0 && nums[i]>=nums[i+1]) {
i--;
}
if(i<0) {
// 如果找不到这么一个位置i,表示整个序列都是降序的,即已经是最后一个排列。
// 重新得到第一个排列的方法是整个数组逆序。
reverse(nums.begin(), nums.end());
return;
}
// 可想而知,为了是变大程度尽量小,i前面的数不应该改动,我们从i后面的数里面挑出一个比i稍大的
// 因为后面是降序,所以可以直接按位置查找
// find the largest index j which statisfies nums[j]>nums[j]
int j=nums.size()-1;
while(nums[j] <= nums[i]) {
j--;
}
// 我们将这个数nums[j]交换到nums[i]的位置
swap(nums[i], nums[j]);
// 因为我们找到的nums[j]是比nums[i]刚刚稍小的数,所以交换完成后,后面的区间[i+1,n]仍然是降序的
// 此时还没有结束。因为后面的区间[i+1,n]是降序的,我们知道降序是最大的排列(最后一个排列)而不是最小的排序
// 我们将这个降序区间调整回为升序区间,此时才是下一个排列
reverse(nums.begin()+i+1, nums.end());
}
};

相关练习题:https://leetcode.cn/problems/next-permutation

参考资料:
https://leetcode.cn/problems/next-permutation/solution/xia-yi-ge-pai-lie-by-leetcode-solution/
https://leetcode.cn/problems/next-permutation/solution/xia-yi-ge-pai-lie-by-powcai/
https://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order

wsl2配置代理

wsl1中主机和wsl共享网络,但在wsl2中是单独的网络。
可以借助如下命令配置代理(可写在~/.profile里)

1
2
3
4
5
6
7
8
9
10
11
host_ip=$(cat /etc/resolv.conf |grep "nameserver" |cut -f 2 -d " ")

# system proxy
export ALL_PROXY="http://$host_ip:10809"

# go proxy
GOPROXY="https://goproxy.cn,direct"

# git proxy
git config --global http.proxy socks5://$host_ip:10808
git config --global https.proxy socks5://$host_ip:10808

记得打开代理软件的接受来自局域网的连接

Ctrl+Z、Ctrl+C、Ctrl+D的区别

  Ctrl+C和Ctrl+Z都是中断命令,但作用不同。

  Ctrl+C是发送SIGINT信号,终止一个进程。

  Ctrl+Z是发送SIGSTOP信号,挂起一个进程,将作业放置到后台(暂停状态)。与此同时,可以通过fg重启前台被中断的任务,也可以通过bg把中断的任务放到后台执行。

  Ctrl+D表示一个特殊的二进制值EOF,代表输入完成或注销。

http://t.zoukankan.com/diantong-p-5566305.html

二分的整理

模板lower_bound & upper_bound

“搜索区间”定义为等于target的区间,对于upper_bound,它的结果是搜索区间外的下一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
// 搜索区间为 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 搜索区间变为 [mid+1, right]
left = mid + 1;
} else if (nums[mid] > target) {
// 搜索区间变为 [left, mid-1]
right = mid - 1;
} else if (nums[mid] == target) {
// 收缩右侧边界
right = mid - 1;
}
}
// 检查出界情况
// 所有可能性:left: [0, n] right: [-1, n-1] mid: [0, n-1]
if (left >= nums.length || nums[left] != target)
return -1;
return left;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 这里改成收缩左侧边界即可
left = mid + 1;
}
}
// 这里改为检查 right 越界的情况,见下图
if (right < 0 || nums[right] != target)
return -1;
return right; // 若要求upper_bound(下一个不等于target的位置)则是return left;
}

1、为什么 while 循环的条件中是 <=,而不是 <?

LeetCode - 954. 二倍数对数组

给定一个长度为偶数的整数数组 arr,只有对 arr 进行重组后可以满足 “对于每个 0 <= i < len(arr) / 2,都有 arr[2 * i + 1] = 2 * arr[2 * i]” 时,返回 true;否则,返回 false。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/array-of-doubled-pairs
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

阅读更多...

分治策略与主定理

== 支配理论 ==
假设有递归关系式 $T(n) = a T\left(\frac{n}{b}\right) + f(n)$,其中$ a \geq 1 \mbox{, } b > 1$

其中,n 为问题规模, a 为递归的子问题数量,$\frac{n}{b}$ 为每个子问题的规模(假设每个子问题的规模基本一样),f(n) 为递归以外进行的计算工作。

=== 情形一 ===
如果存在常数$ \epsilon > 0 $,有

$ f(n) = O\left( n^{\log_b (a) - \epsilon} \right)$(可不严谨地视作多项式地小于)

则: $ T(n) = \Theta\left( n^{\log_b a} \right) $

=== 情形二 ===
如果存在常数$ \epsilon\ge0 $,有: $f(n) = \Theta\left( n^{\log_b a} \log^{\epsilon} n \right)$

则: $T(n) = \Theta\left( n^{\log_b a} \log^{\epsilon+1} n \right) $

=== 情形三 ===
如果存在常数$ \epsilon > 0 $,有: $ f(n) = \Omega\left( n^{\log_b (a) + \epsilon} \right) $(多项式地大于)

同时存在常数$c < 1$以及充分大的$n$,满足:$a f\left( \frac{n}{b} \right) \le c f(n)$

则:$T\left(n \right) = \Theta \left(f \left(n \right) \right)$

== 常用算法中的应用 ==

算法 递归关系式 运算时间 备注
二分搜索 $ T(n) = T\left(\frac{n}{2}\right) + \Theta(1) $ $\Theta(\log n)$ 情形二($\epsilon=0$)
二叉树遍历 $T(n) = 2 T\left(\frac{n}{2}\right) + \Theta(1) $ $\Theta(n)$ 情形一
最佳排序矩阵搜索(已排好序的二维矩阵) $T(n) = 2 T\left(\frac{n}{2}\right) + O(\log n)$
归并排序 $T(n) = 2 T\left(\frac{n}{2}\right) + \Theta(n)$ $\Theta(n\log n)$ 情形二($\epsilon=0$)

最大子数组和

经典题目:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

注意:题目要求子数组最少包含一个元素,所以返回结果可能为负(若所有元素负,则将返回最大的一个负数)。倘若允许子数组为空(结果为0),则要怎么修改?

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

https://leetcode-cn.com/problems/maximum-subarray/

解1 动态规划:

1
2
3
4
5
6
7
8
9
10
int maxSubArray(vector<int>& nums) {
if(nums.empty()) return 0;
size_t n=nums.size();
vector<int> ans(n); // 代表以nums[i]为结尾的连续最大和
ans[0]=nums[0];
for(int i=1;i<n;i++) {
ans[i]=max(ans[i-1]+nums[i],nums[i]);
}
return *max_element(ans.begin(), ans.end());
}

刻画解的状态结构:f(i)代表以nums[i]为结尾的连续最大和
状态转移方程:f(i)=max{ f(i-1)+nums[i], nums[i] }
解释:选择是将nums[i]以nums[i-1]为结尾的最优解,或是选择以nums[i]单独成为解

注:我在第一次尝试解答时,将状态结构f(i)构造为了 代表以nums[1]…num[i]可以包含的最优解,
然而后续遭遇了困难,因为我无法简单确定nums[1]…num[i-1]的最优解位置,可能不在末尾而在中间,难以继续转移。
以后要注意不同的结构构造方法,并且注意怎样构造才更方便更简洁。

解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
int maxCrossingSubArray(vector<int>& nums, int left, int mid, int right) {
int leftSum=0;
for(int i=mid-1, sum=0; i>=left; i--) {
sum+=nums[i];
if(sum > leftSum) {
leftSum = sum;
}
}
int rightSum=0;
for(int i=mid+1, sum=0; i<=right; i++) {
sum+=nums[i];
if(sum > rightSum) {
rightSum = sum;
}
}
return leftSum+nums[mid]+rightSum;
}
int maxSubArray(vector<int>& nums, int left, int right) {
if (left==right) {
return nums[left]; // base case
}
int mid = (left+right)/2;
int rLeft = maxSubArray(nums, left, mid);
int rRight = maxSubArray(nums, mid+1, right);
int rMid = maxCrossingSubArray(nums, left, mid, right);
return max(max(rLeft, rRight), rMid);
}
int maxSubArray(vector<int>& nums) {
return maxSubArray(nums, 0, nums.size()-1);
}

注:
maxCrossingSubArray的实现
注意左右的最大和要分开计算。想想若使用同一个sum变量会发生什么?
要注意解中肯定可能包含负数,并非遇到负数就要中断。如3 -1 5肯定比3大。

解3 分治:
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/maximum-subarray/solution/zui-da-zi-xu-he-by-leetcode-solution/
著作权归作者所有。

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
struct Status {
int lSum, rSum, mSum, iSum;
};

Status pushUp(Status l, Status r) {
int iSum = l.iSum + r.iSum;
int lSum = max(l.lSum, l.iSum + r.lSum);
int rSum = max(r.rSum, r.iSum + l.rSum);
int mSum = max(max(l.mSum, r.mSum), l.rSum + r.lSum);
return (Status) {lSum, rSum, mSum, iSum};
};

Status get(vector<int> &a, int l, int r) {
if (l == r) {
return (Status) {a[l], a[l], a[l], a[l]};
}
int m = (l + r) >> 1;
Status lSub = get(a, l, m);
Status rSub = get(a, m + 1, r);
return pushUp(lSub, rSub);
}

int maxSubArray(vector<int>& nums) {
return get(nums, 0, nums.size() - 1).mSum;
}

该解采用了不同的分治设计,具体可以参见LeetCode答案。

数组中的逆序对

借助归并排序分治问题,在合并解的阶段计数。

归并排序每次将数据分解为nums[p…q]和[q+1…r],分别求解子问题,然后合并。
在合并的过程中可以完成对逆序对的计数,合并过程逐个从左右区间未选取的元素中选取最小的元素放入结果。
我们可以想到:

  • 由于子问题求解完毕后左、右子区间[p…q]、[q+1…r]有序,任意剩余子区间[p+i…q]、[q+1+j…r]也必然有序,所以拿取的最小元素必然是其中一个剩余区间的第一个元素
  • 若元素是从左区间拿取,由于它是最小且在所有左侧、右侧剩余元素的最前,所以与该元素不存在相关的逆序对
  • 若元素是从右区间拿取,由于它是最小且在所有右侧剩余元素的最前,所以左侧剩余元素nums[q+1+j]与右侧第一个剩余元素nums[q+1+j]构成全部相关的逆序对
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
class Solution {
public:
inline int merge(vector<int>& nums, size_t p, size_t q, size_t r) {
if(!(p<=q && q<r)) return 0;
int numRev=0;
size_t n1 = q-p+1, n2 = r-q;
vector<int> A(n1), B(n2);
for(size_t i=0;i<n1;i++) A[i]=nums[p+i];
for(size_t i=0;i<n2;i++) B[i]=nums[q+1+i];
size_t i=0, j=0;
for(size_t c=p;c<=r;c++) {
// 注意判定边界。
// 另外不可以用哨兵值的方法,因为此题输入可能会包含int的最大值
if (i>=n1) {
// 左区间全部用完了,复制右区间
nums[c] = B[j++];
// 此处也不需要增加numRev,左侧已经用完,没有元素再与右侧构成逆序对,numRev增量必然是0
} else if (j>=n2) {
// 右区间全部用完了,复制左区间
nums[c] = A[i++];
} else {
// 都没用完,正常对比
if(A[i] <= B[j]) {
nums[c] = A[i++];
} else {
nums[c] = B[j++];
//if (A[i] != B[j])
numRev += n1-i; // 左侧全部剩余元素A[i...n1]与右侧第一个剩余元素B[j]构成全部相关的逆序对
}
}
}
return numRev;
}
int solve(vector<int>& nums, int p, int r){
if(p>=r) return 0;
int q=(p+r)/2;
int cnt=0;
cnt+=solve(nums,p,q); // 不要忘记加上子问题时发现的逆序对
cnt+=solve(nums,q+1,r); // 不要忘记加上子问题时发现的逆序对
cnt+=merge(nums,p,q,r); // 合并同时也求解本层上的逆序对
return cnt;
}
int reversePairs(vector<int>& nums) {
vector<int> copy(nums);
return solve(copy, 0, nums.size()-1);
}
};

另外在算法导论(第3版第2章思考题2-4.d.)或部分题解上,在merge过程中可能使用了一个极大数作为哨兵值来判断是否其中一个区间已经消耗完毕。

然而,由于题目没有说明数据范围的情况下,我们必须假设输入是全范围的,
也就是说输入可能包括int的最大值2147483647(实际上leetcode上此题测试用例确实包含这个)

因此不可以用大数作为哨兵值的方法

(虽然发现有时测试数据不够强也可能萌混过关)

剑指 Offer 5

请我喝杯咖啡吧~

支付宝
微信