原题:[www.luogu.com.cn/problem/P24…]
题面:
P2448 无尽的生命
题目描述
逝者如斯夫,不舍昼夜!
叶良辰认为,他的寿命是无限长的,而且每天都会进步。
叶良辰的生命的第一天,他有 点能力值。第二天,有 点。第 天,就有 点。也就是 。
但是调皮的小A使用时光机,告诉他第 天和第 天,就可以任意交换某两天的能力值。即 。
小A玩啊玩,终于玩腻了。
叶良辰:小A你给我等着,我有 种办法让你生不如死。除非能在 秒钟之内告知有多少对“异常对”。也就是说,最后的能力值序列,有多少对的两天 ,其中 ,但是能力值 ?
小A:我好怕怕啊。
于是找到了你。
输入格式
第一行一个整数 ,表示小A玩了多少次时光机
接下来 行,,表示将 与 进行交换。
输出格式
输出共一行,表示有多少“异常对”。
输入输出样例 #1
输入 #1
2
4 2
1 4
输出 #1
4
说明/提示
样例说明
- 最开始是
- 然后是
- 然后是
符合的对是 。
- 对于 的数据,;
- 对于 的数据,;
- 对于 的数据,,。
显然本题要求逆序对个数,但是如果只是朴素地使用归并排序等做法枚举所有的 来做,由于 的范围过大,显然会TLE或者直接爆空间。所以我们要思考如何优化这个求逆序对的过程。
平常我们在遇到值较大但数据量较小的情况时,是不是会考虑离散化处理?本题也是一样的,由于 只到 ,而所涉及的所有下标最多为 ,这个量级是可以接受的。所以我们考虑将出现的位置离散化。
设出现的所有位置的个数为 ,则我们将出现的位置按照从小到大的顺序排序,对应的下标为 。然后交换的时候,就直接交换对应的下标,最终得到一个新的下标序列,然后对这个下标序列计算逆序对,最终答案的一部分就由这个最终下标序列的逆序对数贡献。
为什么说是一部分?因为我们要知道,对于任意一对相邻的下标,它们之间映射到原位置后是不连续的,意思是对于原位置 ,它们之间还存在 个元素,而这些元素又可能与交换过的位置间形成逆序对。
我们对一个位置 进行讨论,设经过交换后, 被交换到了第 个下标,而原来在这个下标的位置为 ,那么映射到原来的位置, 之间有多少个位置?是不是就是 个?但由于我们已经计算过交换后下标序列的逆序数,所以如果只用这个数量来计算,由于下标 之间可能也有其他位置,所以会导致重复计算。所以我们要去掉这些“在交换操作中出现过的位置”,也就是 ,所以最终的结果就是 ,其中 即为当前交换完成后的下标, 是原来的下标,可以用一个vector维护。
归并排序
ll merge_sort(vector<ll> &a, int l, int r)
{
if (l >= r)
return 0;
int mid = ((l + r) >> 1);
ll cnt = 0;
cnt += merge_sort(a, l, mid);
cnt += merge_sort(a, mid + 1, r);
vector<ll> temp;
int i = l, j = mid + 1;
while (i <= mid && j <= r)
{
if (a[i] <= a[j])
temp.push_back(a[i++]);
else
{
temp.push_back(a[j++]);
cnt += mid - i + 1;
}
}
while (i <= mid)
temp.push_back(a[i++]);
while (j <= r)
temp.push_back(a[j++]);
for (int k = l; k <= r; k++)
a[k] = temp[k - l];
return cnt;
}
主体部分
cin >> k;
for (int i = 1; i <= k; i++)
{
cin >> x[i] >> y[i];
pos_set.insert(x[i]);
pos_set.insert(y[i]);
}
for (auto x : pos_set)
pos.push_back(x);
sort(pos.begin(), pos.end()); // 离散化位置
vector<ll> orig = pos; // 记录原来的下标位置
auto get_orig = [&](ll val)
{
return lower_bound(orig.begin(), orig.end(), val) - orig.begin();
};
for (int i = 1; i <= k; i++)
swap(pos[get_orig(x[i])], pos[get_orig(y[i])]);
ll ans = 0;
for (int i = 0; i < (int)orig.size(); i++)
{
int j = get_orig(pos[i]);
ll coord_diff = llabs(pos[i] - orig[i]);
int idx_diff = abs(j - i);
ans += coord_diff - idx_diff;
}
ans += merge_sort(pos, 0, (int)pos.size() - 1);
cout << ans;
整体时间复杂度: ,主要在排序和每次查找位置上。
树状数组
同样的,对于求逆序对,我们还可以使用树状数组,这个应该怎么处理呢?
考虑一个序列 ,重新回想一下逆序对的定义,就是那些 且 的数对,则我们先确定位置 ,在它的右边能与它产生逆序对的都是比它小的数,那我们从右往左推,每次统计在当前位置 的右边比它小的数的个数,最后累加起来即为答案。
这个过程怎么维护?在数轴上看一下,从 到 ,其中满足条件的数就是 这些数出现的次数之和。那么显然这是满足前缀性质的,属于一个前缀和计数问题。
那么思路就很显然了,这一块是树状数组的专长,我们维护一个树状数组,每个位置存储这个值出现的次数,然后每次查询 即它的前缀和。但由于位置太大我们无法直接开那么大的数组,所以此时还是要用到离散化,将原位置映射为一串连续的下标,通过下标大小的比较来得出原来位置的大小关系。
树状数组类
class fenwick_tree
{
private:
int n;
vector<ll> bit;
public:
fenwick_tree(const int n)
{
this->n = n;
bit.resize(n + 1, 0);
}
int lowbit(int x)
{
return x & (-x);
}
void update(int idx, ll val)
{
while (idx <= n)
{
bit[idx] += val;
idx += lowbit(idx);
}
}
ll query(int idx)
{
ll sum = 0;
while (idx)
{
sum += bit[idx];
idx -= lowbit(idx);
}
return sum;
}
};
前面处理不变,只是在计算逆序对的方式上选择树状数组
fenwick_tree ft(m);
for (int i = m - 1; i >= 0; i--)
{
int j = get_orig(pos[i]) + 1;
ans += ft.query(j - 1);
ft.update(j, 1);
}
同样的,总体时间复杂度为 ,主要在排序以及树状数组的更新及查询。
几乎没啥差别: