分治递归算法

算法

Count inversions

​ We motivated the problem of counting inversions as a good measure of how different two orderings are. However, one might feel that this measure is too sensitive. Let’s call a pair a significant inversion if i < j i<j i<j and a i > 3 a j a_i>3a_j ai>3aj. Give an O ( n log ⁡ ⁡ n ) O(n\log⁡n) O(nlogn) algorithm to count the number of significant inversions between two orderings.

  • 思想是使用归并排序的思想,在归并的过程中去计数。计数结束的时候,数组也是有序的。
void merge(int a[], int low, int mid, int high, int tmp[])
{
    int i = low;
    int j = mid;
    int index = 0;
    while (i < mid && j < high)
    {
        if (a[i] > a[j])
            tmp[index++] = a[j++];
        else
            tmp[index++] = a[i++];
    }
    while (i < mid)
        tmp[index++] = a[i++];
    while (j < high)
        tmp[index++] = a[j++]; // 到这里两个数组已经排好序,放在tmp数组了
    i = low;
    j = mid;
    index = 0;
    while (i < mid && j < high) // 再遍历一边,用于计数
    {
        if (a[i] > 3 * a[j]) //如果溢出 可以写成 a[i] - (a[j]<<1) > a[j]
        {
            ans += mid - i;
            j++;
        }
        else
            i++;
    }
    for (int i = low; i < high; i++)
        a[i] = tmp[index++];
}

void helper(int a[], int low, int high, int tmp[])
{
    if (low == high - 1)
        return;
    helper(a, low, (low + high) >> 1, tmp);
    helper(a, (low + high) >> 1, high, tmp);
    merge(a, low, (low + high) >> 1, high, tmp);
}

证明

  • loop invariant

    数组 A [ l , k ] A[l, k] A[l,k] 包含了两个子数组 L [ 1 , n 1 ] L[1, n_1] L[1,n1] R [ 1 , n 2 ] R[1, n_2] R[1,n2] 中最小的 k − l + 1 k-l+1 kl+1

  • 初始化: k = l k= l k=l,此时A 为空,循环不变量成立。

  • 维护:

    若L[i] < R[j] 成立,此时A[l, k] 包含了两个子数组中最小的k – l +1 个元素,将

    L[i] 并入数组,A[l, k + 1] 包含了两个子数组中最小的k - l + 2 个元素。新考察的L[i + 1]是目前L 中尚未被归入的元素中的最小元素。若L[i] > R[j] 成立,此时A[l, k] 包含了两个子数组中最小的k - l + 1 个元素,将R[j] 并入数组,A[l, k + 1] 包含了两个子数组中最小的k - l + 2 个元素。新考察的R[j + 1] 是目前R 中尚未被归入的元素中的最小元素。若L[i] >(R[j] * 3) 成立,既有L[i + 1] > L[i] 成立,则L[i + 1] > (R[j] * 3) 成立,对任意TL[i]及其之后的元素这一关系均成立,共有n1 - i + 1 个逆序对。因此,循环不变量成立。

  • 终止:归并完成,k = n1 + n2,其包含了两个子数组的n1 + n2 及对应的所有元素。每一计算得到的逆序对数量的和就是整个数组中逆序对数量。

Rotated array minimum

**使用时间复杂度为 ** O ( log ⁡ n ) O(\log n) O(logn) 的算法,求旋转数组最小值

  • 思想
    在数组 A [ n ] A[n] A[n]中,用 l e f t , r i g h t left, right left,right分别指向数组首尾, m i d mid mid指向数组的中间值。比较 A [ l e f t ] A[left] A[left]和A [ m i d ] [mid] [mid] A [ r i g h t ] A[right] A[right] A [ m i d ] A[mid] A[mid],若 A [ l e f t ] > A [ m i d ] A[left]>A[mid] A[left]>A[mid],即最小值会出现在左侧数组中,则让 r i g h t right right指向原来 m i d mid mid指向的位置上;若 A [ r i g h t ] < A [ m i d ] A[right]<A[mid] A[right]<A[mid],即最小值会出现在右侧数组中,则让 l e f t left left指向 m i d + 1 mid+1 mid+1的位置上。 m i d mid mid继续根据 l e f t left left, r i g h t right right寻找 A [ l e f t ] ∼ A [ r i g h t ] A[left] \sim A[right] A[left]A[right]的中位数,重复上述步骤直到 l e f t , r i g h t left,right left,right重合或者 A [ l e f t ] < A [ m i d ] < A [ r i g h t ] A[left]<A[mid]<A[right] A[left]<A[mid]<A[right]时返回 A [ l e f t ] A[left] A[left],此时 l e f t left left指向的就是数组中的最小值。

    class Solution {
        public int findMin(int[] nums) {
            return func(nums, 0, nums.length - 1);
        }
        public static int func(int[] nums, int l, int r) {
            if (l == r)
                return nums[l];
            int mid = l + (r - l) / 2;
            if (nums[mid] > nums[r])
                return func(nums, mid + 1, r);
            else
                return func(nums, l, mid);
        }
    }
    

正确性证明

  • loop invariant

    考虑当前考察数组左端点$A[left] 所 在 的 ∗ ∗ 严 格 升 序 子 序 列 ∗ ∗ 和 右 端 点 所在的**严格升序子序列**和右端点 A[right] 所 在 的 ∗ ∗ 严 格 升 序 子 序 列 ∗ ∗ 。 允 许 两 个 端 点 在 同 一 个 序 列 的 情 形 。 二 者 处 于 两 个 序 列 时 , 左 侧 序 列 的 值 总 是 大 于 右 侧 序 列 的 值 。 数 组 的 最 小 值 一 定 和 当 前 考 察 数 组 右 端 点 所在的**严格升序子序列**。允许两个端点在同一个序列的情形。二者处于两个序列时,左侧序列的值总是大于右侧序列的值。数组的最小值一定和当前考察数组右端点 A[right]$ 处于同一个严格升序的子序列中。 m i d mid mid 表示当前数组的中点位置。

  • 初始化:考察整个数组, A [ l e f t ] = A [ 0 ] A[left] = A[0] A[left]=A[0] A [ r i g h t ] = A [ n − 1 ] A[right] = A[n - 1] A[right]=A[n1],数组的最小值必然在 A [ n − 1 ] A[n- 1] A[n1] 所在的严格升序子序列中,循环不变量成立。

  • 维护:如果 A [ m i d ] > A [ r i g h t ] A[mid] > A[right] A[mid]>A[right],那么当前中点位置元素在左端点所在的严格升序子序列中。右端点所在的严格升序子序列在 A [ m i d ] A[mid] A[mid] 的右侧,最小值在其右侧。进一步当前考察数组中 A [ m i d ] A[mid] A[mid]的右侧部分,由当前数组左侧子序列的 A [ m i d ] A[mid] A[mid] 右侧部分和右侧子序列组成,最小值仍然在以 A [ r i g h t ] A[right] A[right]为升序的数组中,循环不变量成立。如果 A [ m i d ] < A [ r i g h t ] A[mid] < A[right] A[mid]<A[right],那么当前中点位置元素在右端点所在的严格升序子序列中。最小值可能为 A [ m i d ] A[mid] A[mid],也可能在该子序列的 A [ m i d ] A[mid] A[mid] 左侧。进一步当前考察数组中含 A [ m i d ] A[mid] A[mid] 的左侧部分,由当前数组的左侧子序列和右侧子序列的含 A [ m i d ] A[mid] A[mid] 的左侧部分组成,循环不变量成立。

  • 终止:当前考察数组 l e f t = = r i g h t left == right left==right,循环不变量保持成立,当前考察数组中的唯一元素即为数组的最小值。

证明的核心:定义一个循环不变量

Diameter of binary tree

​ 给定一个二叉树,假设边的长度是1。求解二叉树中任意两节点间距离的最大值。每个TreeNode有三个值分别是Value,指向左子树的指针和指向右子树的指针。输入是树的根节点。

  • 思想:

    m a x D i s t a n c e ( r o o t ) = max ⁡ { d e p t h ( l e f t ) + d e p t h ( r i g h t ) , m a x D i s t a n c e ( l e f t ) , m a x D i s t a n c e ( r i g h t ) } maxDistance(root) = \max\{depth(left)+ depth(right), maxDistance(left), maxDistance(right)\} maxDistance(root)=max{depth(left)+depth(right),maxDistance(left),maxDistance(right)}

    d e p t h ( r o o t ) = max ⁡ { d e p t h ( l e f t ) , d e p t h ( r i g h t ) } + 1 depth(root) = \max \{depth(left),depth(right) \}+ 1 depth(root)=max{depth(left),depth(right)}+1

class Solution {
    public int dis;
    public int diameterOfBinaryTree(TreeNode root) {
        maxDepth(root);
        return dis;
    }
    public int maxDepth(TreeNode root) {
        if (root == null)
            return 0;
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);
        dis = Math.max(dis, left + right);
        return Math.max(left, right) + 1;
    }
}

正确性证明

  • loop invariant

    整棵树 T T T的节点距离最大值在以各个节点为根节点的子树的节点距离最大值中取得。每棵子树的节点距离最大值,可以通过其左侧距离最大值和右侧距离最大值计算获得(求和)。每棵子树的左(右)侧距离最大值,可以通过其左(右)子树两侧最大距离中较大的一个加1获得。每棵子树的空子树对应的距离最大值为0。

  • 初始化:整棵树T 的节点距离最大值为0,尚未遍历的树视为空树,距离为0,循环不变量成立。

  • 维护:如果当前子树为空,则无考察意义,且不影响循环不变量。如果当前子树的左(右)子树为空,对应一侧没有子节点,因此没有有效距离信息(距离为0)。如果当前子树的左(右)子树非空,则可以递归地探索其左右子树,其左(右)侧最大距离信息可以由左(右)子树的最大距离信息获得,得到“更深”的距离信息之后,加上左(右)子树根节点到当前子树根节点的距离(距离为1)。对于当前子树,其在左右两侧各有一个“最深”的节点距离信息,则该子树的最大节点距离可以通过一条由两侧“最深”节点出发到达,经过子树根节点的路径计算得到。每一个子树的最大节点距离信息对应了一个经过该子树根节点的路径,根据树的结构,这条路径只能在这个子树中。因此,整棵树的最大节点距离所在路径,必然在每一个节点所在子树的这些路径中。在对树的后序遍历以及节点距离信息更新的过程中,循环不变量成立。

  • 终止:完成对树 T T T中所有节点的遍历,最后更新根节点对应的距离信息。在循环不变量成立的前提下,可以获得所有节点为根节点子树的最大节点距离信息。根据这些最大节点距离信息,可以获得整棵树的最大节点距离信息。

Local minimum in binary tree

​ 给定一棵完全二叉树,定义若树中的一个点 v v v v a l u e value value,比它相连的其他节点的 v a l u e value value都小,则将这个节点的值作为局部最小值。时间复杂度$O(\log n) $

  • 思想

    1. 根节点比左右孩子节点都小,则返回根节点。

    2. 根节点比左孩子小,比右孩子大。

    3. 根节点比右孩子小,比左孩子大。

    4. 根节点比左右孩子都大。

    当我们选择一条 v a l u e value value逐渐下降的路径,到达节点 v v v,若 v v v的左右孩子都比其大,则返回 v v v。否则直到找到这样的 v v v,或到达叶子节点,由于是沿着 v a l u e value value下降的方向,所以叶子节点是满足条件的。

正确性证明

  • 反证法

    • 假设:存在一棵树不存在满足题意的 L o c a l M i n i m u m LocalMinimum LocalMinimum

    • 对于树高 h h h的完全二叉树,对于任意一条 v a l u e value value下降的路径。我们考察第 h − 2 h-2 h2层的节点为 x [ i ] x[i] x[i] ,考察其两个字节点,则存在 x [ ( i − 1 ) / 2 ] > x [ i ] x[(i-1)/2]>x[i] x[(i1)/2]>x[i],$ x[2 *i +1] < x[i], x[2 * i +2] < x[i] , 这 样 的 ,这样的 ,x[i]$ 节点一定存在,若不存在,说明这个要么是满足题目的点,要么可以继续下降,下降到叶子节点,可以发现叶子节点一定是满足条件的节点,与假设矛盾。

    • 由于叶子节点也不存在满足条件的 L o c a l M i n i m u m LocalMinimum LocalMinimum,所以对于有 x [ 2 ∗ i + 1 ] > x [ i ] , x [ 2 ∗ i + 2 ] > x [ i ] x[2 *i +1] > x[i], x[2 * i +2] > x[i] x[2i+1]>x[i],x[2i+2]>x[i] x [ i ] x[i] x[i]的孙子节点就是叶子节点)矛盾,假设不成立。

    所以,按照算法一定能找到一个符合题意的 L o c a l M i n i m u m LocalMinimum LocalMinimum

Local minimum in grid*

​ 在 n ∗ n n*n nn的网格上找局部最小值,要求时间复杂度为$O(n) $

  • 遍历 G G G的四个边界、 G [ n / 2 ] [ j ] G[n/2][j] G[n/2][j] G [ i ] [ n / 2 ] G[i][n/2] G[i][n/2],将G划分为4部分,找到最小值 G [ m ] [ n ] G[m][n] G[m][n],借鉴上一题的思路,我们希望每次递归调用可以缩小原问题规模:

    若最小值位于四个边界上,那么递归调用最小值所在的那 G / 4 G/4 G/4部分

    若最小值位于 G [ n / 2 ] [ j ] G[n/2][j] G[n/2][j] G [ i ] [ n / 2 ] G[i][n/2] G[i][n/2],比较最小值的邻居,若仍是最小,直接返回该最小值,否则递归调用比该值还小的那 G / 4 G/4 G/4部分

    inspiration

  • 证明仍然可以使用反证法

Polygon partition

求凸多边形有多少种被不同三角形划分的方式

  • 假设凸 n n n 边形的各个顶点为 P 1 , P 2 , P 3 . . . P n P_1,P_2,P_3...P_n P1,P2,P3...Pn,将一条边作为基准边,此处将 P n , P 1 P_n,P_1 Pn,P1 作为基准边,我们知道,如果将凸 n n n 边形划分为三角形, P n , P 1 P_n,P_1 Pn,P1 必定为某一三角形的一条边,那么此三角形的顶点应该是在 P 2 ∼ P n − 1 P_2\sim P_{n-1} P2Pn1 中,假设顶点为 P k P_k Pk,那么此三角形会将凸 n n n 边形划分为左右两个区域,分别为 P 1 , P 2 . . . P k P_1,P_2...P_k P1,P2...Pk P k , P k + 1 . . . P n P_k,P_{k+1}...P_n Pk,Pk+1...Pn,即划分出了一个凸 k k k边形和一个凸 n − k + 1 n-k+1 nk+1边形,将凸 k k k 边形可划分三角形方法数乘以凸 n − k + 1 n-k+1 nk+1 边形可划分三角形数即可,此时将大问题拆解为子问题,假定 n = 2 n=2 n=2 时,方法数为1,我们已知 n = 3 n=3 n=3时,方法数为1,即可通过上述方法验证 n = 2 n=2 n=2 时的假定, 如此将大问题层层划分为子问题,我们即可算出

    F ( n ) = F ( 2 ) ∗ F ( n − 1 ) + F ( 3 ) ∗ F ( n − 2 ) + . . . + F ( n − 1 ) ∗ F ( 2 ) F(n) = F(2)*F(n -1) + F(3)*F(n - 2) +...+ F(n - 1)*F(2) F(n)=F(2)F(n1)+F(3)F(n2)+...+F(n1)F(2)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值