将一组各不相同的数升序排列,每次只能交换相邻的两个数,求最小的交换次数。
这里用线段树解决,参考博客:点击打开博客链接
我们先将原数组每个值附上一个序号index,再将它排序。如题目的例子:
num: 9 1 0 5 4
index: 1 2 3 4 5
排序后:
num: 0 1 4 5 9
index: 3 2 5 4 1
由于排序后num为0的点排在原来数组的第3个,为了将它排到第一个去,至少需要向前移动两次,它也等价于最小的数0之前有2个数比它大(所以要移动两次),将0移到它自己的位置后,我们将0删掉(目的是为了不对后面产生影响)。再看第二大的数1,它出现在原数组的第二个,他之前有一个数比它大所以需要移动一次。这样一直循环下去那么着5个数所需要移动的次数就是:
num: 0 1 4 5 9
次数 2 1 2 1 0
将次数全部要加起来就是最后所需要移动的总次数。
在建一棵树时,不是直接将原来的num放进树里面,而是将它的下标放进树里面,最初每个节点上赋值为1.然后当查找第一个num时,由于是找的下标为3的位置,所以我们就直接找区间[1,3)之间有多少个1(就是求前导和),这里面1的个数就是第一个num=0所要移动的次数,然后我们把0去掉,其实也就是把下标为3的那个1去掉。这样每个值就依次计算出来了。
当然其实只要是想明白了,不用线段树,直接用树状数组写起来会简便很多。(因为每次只需要计算前导和以及去掉某一个点,是对点的操作)。
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int MAXN = 500000 + 10; int total[MAXN << 2], num[MAXN], index[MAXN]; int N; int ql, qr; //ql,qr代表查询区间的左右端点 //查询 [ql , qr] 前导和,o是当前结点编号,L和R是当前结点的左右端点 int query(int o, int L, int R) { int M = L + (R - L) / 2, ans = 0; if (ql <= L && qr >= R) return total[o]; //当前结点完全包含在查询区间内 if (ql <= M) ans += query(2 * o, L, M); //往左走,查询左子树 if (M < qr) ans += query(2 * o + 1, M + 1, R); //往右走,查询右子树 return ans; } int p, v; //代表修改点的位置和要修改的数值 void update(int o, int L, int R) { total[o] += v; //更新树 if (L == R) return; int M = L + (R - L) / 2; if (p <= M) update(2 * o, L, M); //递归更新左子树或者右子树 else update(2 * o + 1, M + 1, R); } //给下标排序的函数 bool cmp(int x, int y) { return num[x] < num[y]; } int main() { while (~scanf("%d", &N), N) { memset(total, 0, sizeof(total)); for (int i = 1; i <= N; i++) { scanf("%d", &num[i]); p = i; //初始化每个结点赋值1,并更新树 v = 1; update(1, 1, N); index[i] = i; //记录下标 } sort(index + 1, index + N + 1, cmp); //将下标按num的大小排序 long long ans = 0; for (int i = 1; i <= N; i++) { ql = 1; //查询区间[ql, qr] <=> [1, index[i]] qr = index[i]; ans += (query(1, 1, N) - 1); //当前位置不算交换一次,减一 p = index[i]; //查询后就将这个数去掉 v = -1; update(1, 1, N); } printf("%lld\n", ans); } return 0; }