解题报告:小和问题和逆序对问题
小和问题
题目描述
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
实例
对于数组[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++]);
}
}
逆序对问题
题目描述
在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对, 请打印所有逆序对的数量。
思路
这道题无论是暴力求解还是更优解法都与小和问题类似,如果发现左区间上有个数比右区间的还要大,需要在不同的位置“打桩”。
代码
void ReversePair(std::vector<int> &v, size_t begin, size_t end, int &num)
{
if(v.size() == 0 || v.size() == 1)
{
num = 0;
return;
}
if(begin == end)
return;
size_t mid = (begin + end) / 2;
ReversePair(v, begin, mid, num);
ReversePair(v, mid+1, end, num);
std::vector<int> temp{};
size_t i = begin;
size_t j = mid + 1;
while(i <= mid && j <= end)
{
if(v[i] <= v[j])
{
temp.push_back(v[i++]);
}
else
{
std::cout<<"i:"<<i<<" j:"<<j<<std::endl;
num += (mid - i + 1);
temp.push_back(v[j++]);
}
}
while(i <= mid)
{
temp.push_back(v[i++]);
}
while(j <= end)
{
temp.push_back(v[j++]);
}
for(size_t ii = begin, jj = 0; ii <= end; ++ii, ++jj)
{
v[ii] = temp[jj];
}
}