解题报告:小和问题和逆序对问题

小和问题

题目描述

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

实例

对于数组[1,3,4,2,5]:
1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,1、3;
2左边比2小的数,1;
5左边比5小的数, 1、 3、 4、2;
所以小和为1+1+3+1+1+3+4+2=16

思路

使用O(n*n)扫描是可以的。暴力法实际上每趟都浪费了一些前几趟得出的隐含的大小关系,所以有待优化。可以想办法使用时间复杂度更好的方法。

等价转化

小和问题可以等价转换为:“如果arr[i]右边有N个数比arr[i]大,就把小和自加arr[i]*N”

和归并排序的关系

如果可以做到局部有序的话,那么遍历做加法就可以转换为乘法,这里联想到归并排序,归并排序有点像二叉树的后序遍历,将兄弟节点有序合并,“上传”到父节点,循环往复,直到根节点为止。(这里的“兄弟节点”实际上代表了两个相邻的子区间)。
思路如下,在子区间归并的过程中“打桩”(当然是升序)。首先左右两边的子区间都是局部有序的,如果左边left[i]比右边的right[j]小,那么意为这右边一共有right.size() - j个数比left[i]大,结合问题的等价转换就很好解决了。

代码

void SmallSum(std::vector<int> &v, size_t begin, size_t end, int &sum)
{
    if(begin == end)
        return;

    size_t mid = (begin + end) / 2;

    // 后序遍历
    SmallSum(v, begin, mid, sum);
    SmallSum(v, mid+1, end, sum);

    std::vector<int> temp{};
    size_t i = begin;
    size_t j = mid + 1;

    while(i <= mid && j <= end)
    {
        if(v[i] < v[j])
        {
            sum += (v[i] * (end - j + 1));  // “打桩”的位置!!!
            temp.push_back(v[i++]);
        }
        else
        {
            temp.push_back(v[j++]);
        }
    }

    while(j <= end)
    {
        temp.push_back(v[j++]);
    }
    while(i <= mid)
    {
        temp.push_back(v[i++]);
    }
}

逆序对问题

题目描述

在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对, 请打印所有逆序对的数量。

思路

这道题无论是暴力求解还是更优解法都与小和问题类似,如果发现左区间上有个数比右区间的还要大,需要在不同的位置“打桩”。

代码

猜你喜欢

转载自www.cnblogs.com/jo3yzhu/p/12521293.html