Binary Search Tree Iterator

Problem

Implement an iterator over a binary search tree (BST). Your iterator will be initialized with the root node of a BST.

Calling next() will return the next smallest number in the BST.

Note: next() and hasNext() should run in average O(1) time and uses O(h) memory, where h is the height of the tree.

Solution

思路是使用栈。

  1. 给定根节点root,从root开始不断向左,将遇到的每个节点入栈
  2. 使用next函数时,栈顶top节点出栈,将top节点设置为root,执行步骤1
  3. hasNext函数:查看栈是否为空即可
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
class BSTIterator {
stack<TreeNode *> myStack;
public:
BSTIterator(TreeNode *root) {
pushAll(root);
}

/** @return whether we have a next smallest number */
bool hasNext() {
return !myStack.empty();
}

/** @return the next smallest number */
int next() {
TreeNode *tmpNode = myStack.top();
myStack.pop();
pushAll(tmpNode->right);
return tmpNode->val;
}

private:
// 从root开始不断向左,将遇到的每个节点入栈
void pushAll(TreeNode *node) {
for (; node != NULL; myStack.push(node), node = node->left);
}
};
分享到

Gibbs Sampling

蒙特卡洛数值积分

如果我们要求f(x)的积分$ \int_a^b f(x) \,dx $,而f(x)的形式比较复杂积分不好求,则可以通过数值解法来求近似的结果。常用的方法是蒙特卡洛积分:这样把q(x)看做是x在区间内的概率分布,而把前面的分数部门看做一个函数,然后在q(x)下抽取n个样本,当n足够大时,可以用采用均值来近似:因此只要q(x)比较容易采到数据样本就行了。随机模拟方法的核心就是如何对一个概率分布得到样本,即抽样(sampling).

Box-Muller 变换

如果随机变量 $U_1,U_2$ 独立且$U_1,U_2 ∼ Uniform[0,1]$,如果有: 则 $Z_0,Z_1$ 独立且服从标准正态分布。

Monte Carlo principle

X 表示随机变量,服从概率分布 p(x), 那么要计算 f(x) 的期望,只需要我们不停从 p(x) 中抽样$x_i$,然后对这些$f(x_i)$取平均即可近似f(x)的期望:其中$E(f)$即为f的期望。

![](http://ww4.sinaimg.cn/large/9bcfe727jw1f9u6nr4wtmj20cx08ht98.jpg)
接受-拒绝抽样(Acceptance-Rejection sampling)

有时候,p(x)是很难直接采样的的。
既然 p(x) 太复杂在程序中没法直接采样,那么我设定一个程序可抽样的分布 q(x) 比如高斯分布,然后按照一定的方法拒绝某些样本,达到接近 p(x) 分布的目的,其中q(x)叫做 proposal distribution。
具体操作如下,设定一个方便抽样的函数 q(x),以及一个常量 k,使得 p(x) 总在 kq(x) 的下方。

  • x 轴方向:从 q(x) 分布抽样得到 a。(如果是高斯,就用之前说过的 tricky and faster 的算法更快)
  • y 轴方向:从均匀分布(0, kq(a)) 中抽样得到 u。
  • 如果刚好落到灰色区域: u > p(a), 拒绝, 否则接受这次抽样
  • 重复以上过程

在高维的情况下,Rejection Sampling 会出现两个问题,第一是合适的 q 分布比较难以找到,第二是很难确定一个合理的 k 值。这两个问题会导致拒绝率很高,无用计算增加。

![](http://ww2.sinaimg.cn/large/9bcfe727jw1f9u6s3wo7hj20fa07vdgd.jpg)
马尔科夫稳态

马氏链即马尔可夫链。社会学家经常把人按其经济状况分成3类:下层(lower-class)、中层(middle-class)、上层(upper-class),我们用1,2,3 分别代表这三个阶层。社会学家们发现决定一个人的收入阶层的最重要的因素就是其父母的收入阶层。如果一个人的收入属于下层类别,那么他的孩子属于下层收入的概率是 0.65, 属于中层收入的概率是 0.28, 属于上层收入的概率是 0.07。事实上,从父代到子代,收入阶层的变化的转移概率如下:

![](http://ww3.sinaimg.cn/large/9bcfe727jw1f9u6zizzbaj20ch06hq2y.jpg)

经过迭代,三种阶层的状态converge to

[0.286, 0.489, 0.225]

马氏链定理

![](http://ww1.sinaimg.cn/large/9bcfe727jw1f9u72rq6c8j20fa0btwfv.jpg)
Markov Chain Monte Carlo (MCMC)

由于马氏链能收敛到平稳分布, 于是一个很的漂亮想法是:如果我们能构造一个转移矩阵为P的马氏链,使得该马氏链的平稳分布恰好是p(x), 那么我们从任何一个初始状态$x_0$出发沿着马氏链转移, 得到一个转移序列 $x_0,x_1,x_2,⋯x_n,x_{n+1}⋯$,, 如果马氏链在第n步已经收敛了,于是我们就得到了 π(x) 的样本$x_n,x_{n+1}⋯$。这是Metropolis算法的基本思想。MCMC 算法是 Metropolis 算法的一个改进变种,马氏链的收敛性质主要由转移矩阵P 决定, 所以基于马氏链做采样的关键问题是如何构造转移矩阵P,使得平稳分布恰好是我们要的分布p(x)。

![](http://ww2.sinaimg.cn/large/9bcfe727jw1f9u7negt9mj20fa0f7tcc.jpg)
假设我们已经有一个转移矩阵Q(对应元素为q(i,j)), 用于采样概率分布p(x)的算法如下:
![](http://ww1.sinaimg.cn/large/9bcfe727jw1f9u7psj1wdj20fa06nmxc.jpg)
![](http://ww2.sinaimg.cn/large/9bcfe727jw1f9u7qj49bbj20fa0fk42d.jpg)
![](http://ww3.sinaimg.cn/large/9bcfe727jw1f9u7r9o55vj20fa07emxe.jpg)
MCMC —— Gibbs Sampling算法
![](http://ww1.sinaimg.cn/large/9bcfe727jw1f9u7s3xbp5j20fa0ddmzx.jpg)
![](http://ww1.sinaimg.cn/large/9bcfe727jw1f9u7sldz89j20ad08qgls.jpg)
![](http://ww4.sinaimg.cn/large/9bcfe727jw1f9u7tgi99tj20fa082q4m.jpg)
![](http://ww2.sinaimg.cn/large/9bcfe727jw1f9u7ueeigyj20fa0f1wj6.jpg)
![](http://ww3.sinaimg.cn/large/9bcfe727jw1f9u7uy86ffj20fa08oaa7.jpg)

以上算法收敛后,得到的就是概率分布$p(x_1,x_2,⋯,x_n)$的样本。

分享到

3Sum Closest

Problem

Given an array S of n integers, find three integers in S such that the sum is closest to a given number, target. Return the sum of the three integers. You may assume that each input would have exactly one solution.

For example, given array S = {-1 2 1 -4}, and target = 1.

The sum that is closest to the target is 2. (-1 + 2 + 1 = 2)
Solution

最简单的莫过于O(n^3)的遍历算法了。
这里介绍一个O(n^2)的方法:

  • 设置first,second和third三个下标。将数组先排序
  • 第一层循环固定住first,将second放在first+1,将third放在最后
  • 计算当前和curSum
    • 如果curSum等于目标值target,直接返回
    • 如果curSum比记录值更好,更新记录值
    • 然后更改second或third。如果curSum大于target,则third—,否则second++
    • 当second和third相遇,内层循环结束。first++迭代

由于second和third这样移动的复杂度为O(n),所以整体的复杂度为O(n^2).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int threeSumClosest(vector<int>& nums, int target) {
if(nums.size() < 3) return 0;
int closest = nums[0]+nums[1]+nums[2];
sort(nums.begin(), nums.end());
for(int first = 0 ; first < nums.size()-2 ; ++first) {
if(first > 0 && nums[first] == nums[first-1]) continue;
int second = first+1;
int third = nums.size()-1;
while(second < third) {
int curSum = nums[first]+nums[second]+nums[third];
if(curSum == target) return curSum;
if(abs(target-curSum)<abs(target-closest)) {
closest = curSum;
}
if(curSum > target) {
--third;
} else {
++second;
}
}
}
return closest;
}
分享到

Find Minimum in Rotated Sorted Array

Problem

Suppose a sorted array is rotated at some pivot unknown to you beforehand.

(i.e., 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2).

Find the minimum element.

You may assume no duplicate exists in the array.

Solution

将数组旋转,则会将小元素转到数组后面。
这是一个经典的二叉搜索问题,考虑如下:
对于位于下标[start, end]范围内的元素,如果有

  • a[start] < a[end]: 那么从start到end范围内的元素是有序的,第一个元素便是其最小值
  • 否则,看mid = start+(end-start)/2
    • 如果a[start] < a[mid],那么旋转位置在(mid, end)之间,这时设置 start = mid+1
    • 否则,旋转位置在(start, mid)之间, 这时设置 end = mid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int findMin(vector<int> &num) {
int start=0,end=num.size()-1;

while (start<end) {
if (num[start] < num[end])
return num[start];
int mid = start+(end-start)/2;
if (num[mid] > num[start]) {
start = mid+1;
} else {
end = mid;
}
}

return num[start];
}
Extension
重复元素

如果存在元素重复现象怎么办。即有可能出现没法判断应该向左还是向右的情况,所以简单的做法是将end-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int findMin(vector<int> &num) {
int lo = 0;
int hi = num.size() - 1;
int mid = 0;

while(lo < hi) {
mid = lo + (hi - lo) / 2;

if (num[mid] > num[hi]) {
lo = mid + 1;
}
else if (num[mid] < num[hi]) {
hi = mid;
}
else { // when num[mid] and num[hi] are same
hi--;
}
}
return num[lo];
}
};
二叉搜索的标准写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int binary_search(int *a, int left, int right, int target) {
// 循环结束判断
while (left <= right) {
int mid = left + (right-left)/2;
if (a[mid] == target)
return mid;
// 如果left=2, right=3,并且a[2]<target是将陷入死循环
if (a[mid] < target)
left = mid+1;
else
right = mid-1;
}

}
分享到

Split Array Largest Sum

Problem

Given an array which consists of non-negative integers and an integer m, you can split the array into m non-empty continuous subarrays. Write an algorithm to minimize the largest sum among these m subarrays.

Note:
Given m satisfies the following constraint: 1 ≤ m ≤ length(nums) ≤ 14,000.

Examples:

Input:
nums = [1,2,3,4,5]
m = 2

Output:
9

Explanation:
There are four ways to split nums into two subarrays.
The best way is to split it into [1,2,3] and [4,5],
where the largest sum among the two subarrays is only 9.

Solution

普通的递归方法会超时!!!

首先确定任意合法的m值,对应结果的范围[left ~ right]
其中left = max(nums[]), right = sum(nums[]).

这里考虑使用二分查找法,如果left = right,那么返回left就好
给定一个可能的结果mid = left+(right-left)/2.,验证是否合法

判断一个x是否合法的方法如下:
顺序扫描nums[],计算块和不大于(尽可能接近)mid的个数c
如果c > m,那么mid应该变大,相应的c变小;反之,c <= m,说明mid应该变小,相应c变大

如果合法:mid应该变小,即 right = mid;
否则,mid应该变大,即 left = mid + 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
31
32
class Solution {
public:
// check if nums can be divided into m subsets s.t each subset's summation <= sum
bool canSplit(vector<int>& nums, int m, long sum) {
int c = 1;
long s = 0;
for (auto& num : nums) {
s += num;
if (s > sum) {
s = num; // first element
++c; // a new subset
}
}
// c<=m indicates nums can be divided into m subsets s.t each subset's summation <= sum
return c <= m;
}

int splitArray(vector<int>& nums, int m) {
long left = 0, right = 0;
for (auto& num : nums) {
left = max(left, (long)num);
right += num;
}
while (left < right) {
if (left == right) return left; // answer found
long mid = left + (right-left)/2;
if (canSplit(nums, m, mid)) right = mid; // be smaller.
else left = mid+1; // be bigger
}
return left;
}
};
分享到

Trapping Rain Water

一维情况

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.

For example
Given [0,1,0,2,1,0,1,3,2,1,2,1], return 6.


The above elevation map is represented by array [0,1,0,2,1,0,1,3,2,1,2,1]. In this case, 6 units of rain water (blue section) are being trapped. Thanks Marcos for contributing this image!

解决方案
  • 双指针,分别指向数组两端,主要是由两边到中间的思想
  • 维护左边最高、右边最高的值
  • 如果: a[left] < a[right],先处理left-side [ 否则,right-side。这样保证未被处理的一边总有大于另一边最大值的元素。]
    • 假如a[left] >= maxleft, 那么: maxleft = a[left]
    • 否则:ret += maxleft-a[left]. 因为右边总有比maxleft大的,否则,不可能处理left这边的数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int trap(int A[], int n) {
int left=0; int right=n-1;
int res=0;
int maxleft=0, maxright=0;
while(left<=right){
// 先处理小的一方,保证对面方总有更大的数
if(A[left]<=A[right]){
// 更新最大值
if(A[left]>=maxleft) maxleft=A[left];
// 因为另一边总有大于maxleft的值,所以,此时可盛水maxleft-A[left]
else res+=maxleft-A[left];
left++;
} else { // 同左边
if(A[right]>=maxright) maxright= A[right];
else res+=maxright-A[right];
right--;
}
}
return res;
}
};
二维状况

Given an m x n matrix of positive integers representing the height of each unit cell in a 2D elevation map, compute the volume of water it is able to trap after raining.

Note:
Both m and n are less than 110. The height of each unit cell is greater than 0 and is less than 20,000.

Example:

Given the following 3x6 height map:
[
      [1,4,3,1,3,2],
      [3,2,1,3,2,4],
      [2,3,3,2,3,1]
]

Return 4.


The above image represents the elevation map [[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]

解决方案
从四周开始,由外向内的方法
  • 从边界开始,挑选最矮的已经访问过的cell,检查它的邻居(没有被访问过的)
  • 如果邻居比当前的矮,收集邻居能手机的水量(邻居不必当前矮则无法收集),并填充能收集水的邻居(更新其高度)
  • 把所有的邻居加入到队列
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
public class Solution {

public class Cell {
int row;
int col;
int height;
public Cell(int row, int col, int height) {
this.row = row;
this.col = col;
this.height = height;
}
}

public int trapRainWater(int[][] heights) {
if (heights == null || heights.length == 0 || heights[0].length == 0)
return 0;

PriorityQueue<Cell> queue = new PriorityQueue<>(1, new Comparator<Cell>(){
public int compare(Cell a, Cell b) {
return a.height - b.height;
}
});

int m = heights.length;
int n = heights[0].length;
boolean[][] visited = new boolean[m][n];

// Initially, add all the Cells which are on borders to the queue.
for (int i = 0; i < m; i++) {
visited[i][0] = true;
visited[i][n - 1] = true;
queue.offer(new Cell(i, 0, heights[i][0]));
queue.offer(new Cell(i, n - 1, heights[i][n - 1]));
}

for (int i = 0; i < n; i++) {
visited[0][i] = true;
visited[m - 1][i] = true;
queue.offer(new Cell(0, i, heights[0][i]));
queue.offer(new Cell(m - 1, i, heights[m - 1][i]));
}

// from the borders, pick the shortest cell visited and check its neighbors:
// if the neighbor is shorter, collect the water it can trap and update its height as its height plus the water trapped
// add all its neighbors to the queue.
int[][] dirs = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
int res = 0;
while (!queue.isEmpty()) {
Cell cell = queue.poll();
for (int[] dir : dirs) {
int row = cell.row + dir[0];
int col = cell.col + dir[1];
if (row >= 0 && row < m && col >= 0 && col < n && !visited[row][col]) {
visited[row][col] = true;
res += Math.max(0, cell.height - heights[row][col]);
queue.offer(new Cell(row, col, Math.max(heights[row][col], cell.height)));
}
}
}

return res;
}
}
Dijkstra

构建一个图,每个cell都是一个节点,再加上一个dummy节点表示外部区域。
如果cell(i, j)与cell(i’, j’)相邻,那么,创建一个由cell(i, j)到cell(i’, j’)的有向边,边的权值为height(i, j)
边上cell与dummy节点有一条权值为0的边。
设定每条路径的权值为:该路径上最大的权值。所以对每一个cell(i,j),它能盛的水就是cell(i, j)到dummy节点的最短路径的权值dist(i, j)
如果:dist(i, j) <= height(i, j),那么cell(i, j)不能盛水。

我们可能需要计算dist(i, j) 对于每个(i, j)对,即多个cell,只有一个终点dummy节点。所以将每条边的方向逆转,那么只需要使用一次Dijkstra算法就可以计算dummy节点到每个cell的最短路径,继而计算出每个cell能盛水的量(最短路径的权值)。

假设cell由r行、c列,那么时间复杂度:O(rclog(rc)) = O(rcmax(log r, log c)),空间复杂度为:O(rc).

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
public class Solution {

int[] dx = {0, 0, 1, -1};
int[] dy = {1, -1, 0, 0};

List<int[]>[] g;
int start;

private int[] dijkstra() {
int[] dist = new int[g.length];
Arrays.fill(dist, Integer.MAX_VALUE / 2);
dist[start] = 0;
TreeSet<int[]> tree = new TreeSet<>((u, v) -> u[1] == v[1] ? u[0] - v[0] : u[1] - v[1]);
tree.add(new int[]{start, 0});
while (!tree.isEmpty()) {
int u = tree.first()[0], d = tree.pollFirst()[1];
for (int[] e : g[u]) {
int v = e[0], w = e[1];
if (Math.max(d, w) < dist[v]) {
tree.remove(new int[]{v, dist[v]});
dist[v] = Math.max(d, w);
tree.add(new int[]{v, dist[v]});
}
}
}
return dist;
}

public int trapRainWater(int[][] a) {
if (a == null || a.length == 0 || a[0].length == 0) return 0;
int r = a.length, c = a[0].length;

start = r * c;
g = new List[r * c + 1];
for (int i = 0; i < g.length; i++) g[i] = new ArrayList<>();
for (int i = 0; i < r; i++)
for (int j = 0; j < c; j++) {
if (i == 0 || i == r - 1 || j == 0 || j == c - 1) g[start].add(new int[]{i * c + j, 0});
for (int k = 0; k < 4; k++) {
int x = i + dx[k], y = j + dy[k];
if (x >= 0 && x < r && y >= 0 && y < c) g[i * c + j].add(new int[]{x * c + y, a[i][j]});
}
}

int ans = 0;
int[] dist = dijkstra();
for (int i = 0; i < r; i++)
for (int j = 0; j < c; j++) {
int cb = dist[i * c + j];
if (cb > a[i][j]) ans += cb - a[i][j];
}

return ans;
}
}
分享到

Factorial

问题 POJ 1401

给一个数n, 求出n! 有多少个后导零

解决方案

因为n!中的5因子比2多,所以只需要找5的个数就好。

令f(x)表示正整数x末尾所含有的“0”的个数,则有:
      当0 < n < 5时,f(n!) = 0;
      当n >= 5时,f(n!) = k + f(k!), 其中 k = n / 5(取整)
1
2
3
4
5
6
7
8
int countZero(int N) {
int ret = 0;
while (N) {
ret += N/5;
N /= 5;
}
return ret;
}

如果是要求n!中k因子的个数,那么有:

f(n) = x + f(x), where x = n / k.
分享到

Wildcard Matching

Problem

Implement wildcard pattern matching with support for ‘?’ and ‘*’.

‘?’ Matches any single character.
‘*’ Matches any sequence of characters (including the empty sequence).

The matching should cover the entire input string (not partial).

The function prototype should be:
bool isMatch(const char s, const char p)

Some examples:
isMatch("aa","a") → false
isMatch("aa","aa") → true
isMatch("aaa","aa") → false
isMatch("aa", "*") → true
isMatch("aa", "a*") → true
isMatch("ab", "?*") → true
isMatch("aab", "c*a*b") → false
Solution

C++的DP方法超时(好像java可以)。
线性时间算法的基本思想是维护两个指针p1、p2,分别指向s和p,分以下几种情况:

  • p[p2] == ‘?’ 说明匹配,那么 ++p1,++p2
  • p[p2] == ‘*’ 这也是一种匹配,但是可能的情况很多,用start保存此星号的位置,用matched保存与当前星号匹配的s的位置,p2++,p1不移动,先把与星号匹配的设为空,因为如果后面不匹配的话,还会回来给星号匹配字符
  • 否则,当前已经不能匹配了,找到上一个星号位置
    • 上一个星号存在,则p回退到上一个星号后一位,把matched对应的字符串匹配给星号,s回退到matched下一位,matched++,p2=star+1,p1=++matched
    • 否则,没有可能的匹配了,返回 false
  • 最后,如果p2还没到结尾,如果p2及其后面都是’*’,那么匹配成功,否则失败
1
2
3
4
5
6
7
8
9
10
11
bool isMatch(string s, string p) {
int star=-1, s1=0, p1=0, matched=0;
while (s1 < s.length()) {
if (p[p1]=='?' || s[s1]==p[p1]) {++s1, ++p1;}
else if (p[p1] == '*') {star = p1++; matched = s1;}
else if (star>=0) {p1 = star+1; s1 = ++matched;}
else return false;
}
while (p1<p.length() && p[p1]=='*') ++p1;
return p1==p.length();
}
分享到

Segment Tree

线段树


线段树(英语:Segment Tree)是一种二叉搜索树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

对于线段树中的每一个非叶子节点[a,b],它的左子树表示的区间为[a,(a+b)/2],右子树表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树。叶节点数目为N,即整个线段区间的长度。

给定整个线段区间,建立一棵线段树的时间复杂度是O(N)。单点修改的时间复杂度是O(logN) 。单点查询的时间复杂度是O(1)。如果允许惰性赋值而加上延迟标记的话,许多的区间修改的时间复杂度也会是 O(log N),但是单点查询的时间复杂度会变成O(log N)。

线段树的每个节点上往往都增加了一些其他的域。在这些域中保存了某种动态维护的信息,视不同情况而定。这些域使得线段树具有极大的灵活性,可以适应不同的需求。

动态结构
struct node{
     node* left;
     node* right;
    ……
}

动态结构的方法在Range Sum Query中。

静态数组型结构:maxn是最大区间数,而节点数要开4倍多

struct node{
      int left;
      int right;
    ……
}Tree[maxn*4+5]

使用静态数组结构(从1开始计数),left、right表示其左右孩子在数组中对应的位置,如果当前节点的位置时i,那么左孩子2i,右孩子2i+1。

构造方法:主要思想是递归构造,如果当前节点记录的区间只有一个值,则直接赋值,否则递归构造左右子树,最后回溯的时候给当前节点赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
node: 当前节点在静态数组segTree中的位置
begin:题目给定数组的下标起始位置
end: 题目给定数组的下标终止位置
*/
void build(int node, int begin, int end) {
if (begin == end)
segTree[node] = array[begin]; /* 只有一个元素,节点记录该单元素 */
else {
/* 递归构造左右子树 */
build(2*node, begin, (begin+end)/2);
build(2*node+1, (begin+end)/2+1, end);

/* 回溯时得到当前node节点的线段信息 */
if (segTree[2 * node] <= segTree[2 * node + 1])
segTree[node] = segTree[2 * node];
else
segTree[node] = segTree[2 * node + 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
/*
node:当前查询节点
begin,end:当前节点存储的区间,即当前结点的范围
left,right:此次query所要查询的区间
*/
int query(int node, int begin, int end, int left, int right) {
int p1, p2;

/* 查询区间和要求的区间没有交集 */
if (left > end || right < begin)
return -1;

// 查询范围和当前结点的范围重合
if (begin == left && end == right)
return segTree[node];

// 分别查询当前结点的左右孩子
p1 = query(2 * node, begin, (begin + end) / 2, left, right);
p2 = query(2 * node + 1, (begin + end) / 2 + 1, end, left, right);

/* return the expect value */
if (p1 == -1)
return p2;
if (p2 == -1)
return p1;
return p1 + p2;
}

单节点更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
node:当前节点的下标
left,right:当前节点的表示范围
ind,add:将原数组中ind位置元素增加add
*/
void Updata(int node, int left, int right, int ind, int add) {
// 当前是叶节点了,直接更新
if( begin == end ) {
segTree[node] += add;
return ;
}
int m = ( left + right ) >> 1;
if(ind <= m)
Updata(node * 2,left, m, ind, add);
else
Updata(node * 2 + 1, m + 1, right, ind, add);
/*回溯更新父节点*/
segTree[node] = segTree[node * 2] + segTree[node * 2 + 1];

}

Range Sum Query - Mutable

Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), inclusive.

The update(i, val) function modifies nums by updating the element at index i to val.
Example:
Given nums = [1, 3, 5]

sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8
Note:

  • The array is only modifiable by the update function.
  • You may assume the number of calls to update and sumRange function is distributed evenly.

使用直接的方法处理,在有大量的查询(O(N)复杂度)和更新时时间复杂度过高。使用线段树处理的话,查询和修改的复杂度均为O(logN). 本题中,每个节点包含左右孩子节点指示自己的范围,还包含一个表示以该节点为根的子树的所有元素的和。

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
public class NumArray {
SegmentTreeNode root = null;
// 线段树的节点,包含左右孩子,和其全部孩子值的和
class SegmentTreeNode {
int sum;
int start, end;
public SegmentTreeNode left, right;

public SegmentTreeNode(int start, int end) {
this.start = start;
this.end = end;
this.left = null;
this.right = null;
this.sum = 0;
}
}
// 将数据index位置的值转换成value,树中只需要更改sum值就好
public void Update(SegmentTreeNode root, int index, int value) {
// 到叶节点时,直接修改sum值
if (root.start == root.end)
root.sum = value;
else { // 非叶节点,要考虑修改树的中间结点
int mid = root.start + (root.end - root.start) / 2;
if (index <= mid) // 如果需要修改的节点在左子树,右子树不需要修改
Update(root.left, index, value);
else // 只修改右子树
Update(root.right, index, value);
// 修改根节点的sum值
root.sum = root.left.sum + root.right.sum;
}
}
// 返回从索引start到end的值的和
public int SumRange(SegmentTreeNode root, int start, int end) {
// root对应的范围正好是查询的范围
if (root.start == start && root.end == end)
return root.sum;
int mid = root.start + (root.end - root.start) / 2;
// 查询范围在左子树上
if (end <= mid)
return SumRange(root.left, start, end);
// 查询范围在右子树上
else if (start > mid)
return SumRange(root.right, start, end);
// 查询范围在左、右子树上
return SumRange(root.left, start, mid) + SumRange(root.right, mid + 1, end);
}

public SegmentTreeNode ConstructSegmentTree(int[] nums, int start, int end) {
if (start > end)
return null;
SegmentTreeNode ret = new SegmentTreeNode(start, end);
if (start == end) {
ret.sum = nums[start];
} else {
int mid = start + (end - start) / 2;
ret.left = ConstructSegmentTree(nums, start, mid);
ret.right = ConstructSegmentTree(nums, mid + 1, end);
ret.sum = ret.left.sum + ret.right.sum;
}
return ret;
}

public NumArray(int[] nums) {
root = ConstructSegmentTree(nums, 0, nums.length - 1);
}

void update(int i, int val) {
Update(root, i, val);
}

public int sumRange(int i, int j) {
return SumRange(root, i, j);
}
}
线段(可能重合)的总长度

思路是在线段可能出现的范围内建立线段树,节点上设置一个表示此节点是否被覆盖的属性cover,cover=1表示该结点所对应的区间被完全覆盖,cover=0表示该结点所对应的区间未被完全覆盖。如下图的线段树,添加线段[1,2][3,5][4,6]

分享到

Binary Indexed Tree

定义

树状数组(Binary Indexed Tree, BIT)用来求区间元素和,求一次区间元素和的时间效率为O(logn)
树状数组是一个可以很高效的进行区间统计的数据结构。在思想上类似于线段树,比线段树节省空间,编程复杂度比线段树低,但适用范围比线段树小。

问题定义:

有n个元素的数组。可能的操作为

1.改变数组下标k的元素

2.查询下标 i~j 的元素和

用树状数组,对操作1和2的时间复杂度都为O(logn)。

算法内容
利用树状数组求前i个元素的和S[i]


对给定序列:A[1]~A[8],构建一个树状数组,如上图,其中,C[]是树状数组,S[k]表示从1-k的元素和。
分析C[]的组成如下:

C[1]=A[1];
C[2]=A[1]+A[2];
C[3]=A[3];
C[4]=A[1]+A[2]+A[3]+A[4];
C[5]=A[5];
C[6]=A[5]+A[6];
C[7]=A[7];
C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

将数组下标转换成二进制,可得:

1 --> 00000001
2 --> 00000010
3 --> 00000011
4 --> 00000100
5 --> 00000101
6 --> 00000110
7 --> 00000111
8 --> 00001000

结论:下标i的二进制中的从右往左数有连续的x个“0”,那么C[i]为序列A[]中的第i-2^x+1到第i个元素的和,即:

C[i] = A[i-2^x+1] + … + A[i], 其中x为i的二进制中的从右往左数有连续“0”的个数

对于每个i,求x的方法是:2^x = i & (-i)

证明:设A’为A的二进制反码,i的二进制表示成A1B,其中A不管,B为全0序列。那么-i=A’0B’+1。由于B为全0序列,那么B’就是全1序列,所以-i=A’1B,所以:i&(-i)= A1B& A’1B=1B,即2^x的值。

所以,S[i]的方法是:

1
2
3
4
5
6
7
8
9
//返回前i个元素和
int Sum(int i) {
int s=0;
while (i > 0) {
s += C[i];
i -= i & (-i);
}
return s;
}

更新C[]

如果A[i]被改变了,那么所有包含A[i]的C[]都要更改,比如,A[3]被改变了,C[3], C[4], C[8]都得修改。
如果A[i]被改变了,那么C[i]必须要更改,并且假设C[k]是C[i]的直接父亲,那么C[k]包含的元素是C[i]包含的元素的2倍,所以:

k = i + 2^x(x是i的元素数)。

所以更新的方法是:

1
2
3
4
5
6
7
// A[i]的改变值为value
void Update(int i,int value) {
while(i<=n) {
C[i] += value;
i += i & (-i);
}
}

二维树状数组

BIT可用为二维数据结果。假设你有一个带有点的平面(有非负的坐标)。合理的操作有三种:

1.在(x , y)设置点
2.从(x , y)移除点
3.在矩形(0 , 0), (x , y)计算点数 - 其中(0 , 0)为左下角,(x , y)为右上角,而边是平行于x轴和y轴。

对于1操作,在(x,y)处设置点,即Update(x,y,1),因为x,y坐标是离散的,所以我们分别对x,y进行更新即可,函数如下

1
2
3
4
5
6
7
8
9
10
void Update(int x,int y,int val) {
while(x<=n) {
int y1 = y;
while (y1 <= n) {
C[x][y1] += val;
y1 += y1 & (-y1);
}
x += x & (-x);
}
}

根据Update可以推得:GetSum函数为:

1
2
3
4
5
6
7
8
9
10
11
12
int GetSum(int x,int y) {
int sum=0;
while (x > 0) {
int y1 = y;
while (y1 > 0) {
sum += C[x][y1];
y1 -= y1 & (-y1);
}
x -= x & (-x);
}
return sum;
}

应用
POJ 2352 Stars

给定星星的坐标(y递增,若y相等,x递增),每个星星都有一个等级,规定它的等级就是在它左下方的星星的个数
输入所有星星后,依次输出等级为0到n-1的星星的个数。

解法:

  • 因为数据按y排序(y相等按x排序),所以对第i个星星,之前的星星没有y值大于当前y值的,如果之前的星星k的x值小于当前x值,那么星星k就是对星星i的等级贡献1。并且,星星i后面的星星不可能贡献星星i的等级。
  • 所以,计算一个星星的等级就应该在这个星星没正式加入之前,计算之前哪些星星在它左下角,y值严格非递减就不用考虑了,只计数x坐标小于等于当前的个数,可以令A[i]的值是已经加入星星中x坐标为i的星星的数量,i的等级即为 Sum(i);
  • 每次加入一个星星,加入星星i之前,计算星星i的贡献,即出现在星星i左下角的星星个数,也即数组A[]中小于等于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
#include<stdio.h>
#include<string.h>
#define n 32001
int c[n+5], total[n+5];
int Lowbit(int t) {
return t&(t^(t-1));
}
int Sum(int end) {
int sum = 0;
while(end > 0) {
sum += c[end];
end -= Lowbit(end);
}
return sum;
}
void add(int li, int val) {
while(li<=n) {
c[li] += val;
li += Lowbit(li);
}
}
int main() {
int i, j, x, y, nn;
scanf("%d", &nn);
memset(c, 0, sizeof(c));
memset(total, 0, sizeof(total));
for(i=1; i<=nn; i++) {
scanf("%d%d", &x, &y); //由于坐标x可能为0,因此输入坐标要+1,不然会超时0&(-0)=0;
add(x+1, 1);
total[Sum(x+1)-1]++;
}
for(i=0; i<nn; i++)
printf("%d\n", total[i]);
}

本题用线段树也可以做,用静态数组结构,开始时就把所有的元素当成0,加入节点,就是一种更新操作。在把A[]抽象出来后,每次加入一个点时,计算Sum,然后更新节点。

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
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;
int sum[200000];
struct node {
int x,y;
} p[20000];
void push_up(int root) {
sum[root]=sum[root*2]+sum[root*2+1];
}
void update(int root,int l,int r,int p,int v) {
int mid=(l+r)/2;
if(l==r) {
sum[root]++;
return;
}
if(p<=mid)update(root*2,l,mid,p,v);
else update(root*2+1,mid+1,r,p,v);
push_up(root);
}
int q_sum(int root,int l,int r,int ql,int qr) {
if(ql>r||qr<l)return 0;
if(ql<=l&&r<=qr)return sum[root];
int mid=(l+r)/2;
return q_sum(root*2,l,mid,ql,qr)+q_sum(root*2+1,mid+1,r,ql,qr);
}
int main() {
int n,i,j,m=32000;
int _hash[20000];
scanf("%d",&n);
memset(_hash,0,sizeof(_hash));
for(i=0; i<n; i++) {
scanf("%d%d",&p[i].x,&p[i].y);
_hash[q_sum(1,0,m,0,p[i].x)]++;
update(1,0,m,p[i].x,1);
}
for(i=0; i<n; i++)
printf("%d\n",_hash[i]);
return 0;
}
二维树状数组

问题描述:

一个由数字构成的大矩阵,能进行两种操作
1) 对矩阵里的某个数加上一个整数(可正可负)
2) 查询某个子矩阵里所有数字的和,要求对每次查询,输出结果

一维扩展到二维的情况:(lowbit(x)=x&(-x))

C[x][y] = ∑ a[i][j], 其中
    x-lowbit(x) + 1 <= i <= x
    y-lowbit(y) + 1 <= j <= y

在这样的定义下有:

Sun(1,1)=C[1][1];  Sun(1,2)=C[1][2]; Sun(1,3)=C[1][3]+C[1][2];...
 Sun(2,1)=C[2][1];  Sun(2,2)=C[2][2]; Sun(2,3)=C[2][3]+C[2][2];...
 Sun(3,1)=C[3][1]+C[2][1]; Sun(3,2)=C[3][2]+C[2][2];

求和Sum:

1
2
3
4
5
6
7
8
9
int Sum(int i, int j){
int result = 0;
for (int x = i; x > 0; x -= lowbit(x)) {
for(int y = j; y > 0; y -= lowbit(y)) {
result += C[x][y];
}
}
return result;
}

更新update

1
2
3
4
5
6
7
8
private void Modify(int i, int j, int delta){
A[i][j]+=delta;
for(int x = i; x< A.length; x += lowbit(x)) {
for(int y = j; y <A[i].length; y += lowbit(y)) {
C[x][y] += delta;
}
}
}

POJ 2155

给定MxN矩阵,每个元素取值{0, 1},合法的操作如下:

  • 将左上角坐标为(x1, y1),右下角坐标为(x2, y2)的矩形区域内的所有元素反转(0->1, 1->0)
  • 查询A[i][j]的值

在树状数组中存储该节点的变换次数,因为数值只是0或1,所以奇数次的效果是一样的,偶数次的效果也是一样的。如下图所示:
反转[(x1, y1), (x2, y2)]等价于:
分别反转 [(x1, y1), (n,n)], [(x2+1, y2+1), (n,n)], [(x1, y2+1), (n,n)], [(x1+1, y2), (n,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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
const int MAX = 1010;
int c[MAX][MAX];
int n;
int Lowbit(int x) {
return x & (-x);
}

// 是将以(n,n)为右下角,(x,y)为左上角的矩形区域反转
// 需要把所有包含(x, y)的c数组元素更新
void Updata(int x,int y) {
int i,k;
for(i=x; i<=n; i+=Lowbit(i))
for(k=y; k<=n; k+=Lowbit(k))
c[i][k]++;
}

// 包含点(x, y)的所有区间的总反转次数
// 因为反转都是左上->右下,所以包含点(x, y)的区间端点不会在(x, y)右、下角
int Get(int x,int y) {
int i,k,sum = 0;
for(i=x; i>0; i-=Lowbit(i))
for(k=y; k>0; k-=Lowbit(k))
sum += c[i][k];
return sum;
}
int main() {
int ncases,m;
int x1,y1,x2,y2;
char ch[2];
scanf("%d",&ncases);
while( ncases-- ) {
memset(c,0,sizeof(c));
scanf("%d%d",&n,&m);
while( m-- ) {
scanf("%s",ch);
if( ch[0] == 'C' ) {
scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
x1++; y1++; x2++; y2++;
Updata(x2,y2);
Updata(x1-1,y1-1);
Updata(x1-1,y2);
Updata(x2,y1-1);
} else {
scanf("%d%d",&x1,&y1);
printf("%d/n",Get(x1,y1)%2);
}
}
printf("/n");
}
return 0;
}
分享到