洛谷P2448 无尽的生命 (逆序对+离散化巧解)

89 阅读5分钟

原题:[www.luogu.com.cn/problem/P24…]

题面:

P2448 无尽的生命

题目描述

逝者如斯夫,不舍昼夜!

叶良辰认为,他的寿命是无限长的,而且每天都会进步。

叶良辰的生命的第一天,他有 11 点能力值。第二天,有 22 点。第 nn 天,就有 nn 点。也就是 Si=iS_i=i

但是调皮的小A使用时光机,告诉他第 xx 天和第 yy 天,就可以任意交换某两天的能力值。即 SxSyS_x\leftrightarrow S_y

小A玩啊玩,终于玩腻了。

叶良辰:小A你给我等着,我有 100100 种办法让你生不如死。除非能在 11 秒钟之内告知有多少对“异常对”。也就是说,最后的能力值序列,有多少对的两天 x,yx,y,其中 x<yx<y,但是能力值 Sx>SyS_x>S_y

小A:我好怕怕啊。

于是找到了你。

输入格式

第一行一个整数 kk,表示小A玩了多少次时光机

接下来 kk 行,xi,yix_i,y_i,表示将 SxiS_{x_i}SyiS_{y_i} 进行交换。

输出格式

输出共一行,表示有多少“异常对”。

输入输出样例 #1

输入 #1

2
4 2
1 4

输出 #1

4

说明/提示

样例说明

  • 最开始是 1,2,3,4,5,61,2,3,4,5,6\cdots
  • 然后是 1,4,3,2,5,61,4,3,2,5,6\cdots
  • 然后是 2,4,3,1,5,62,4,3,1,5,6\cdots

符合的对是 (1,4),(2,3),(2,4),(3,4)(1,4),(2,3),(2,4),(3,4)

  • 对于 30%30\% 的数据,xi,yi2×103x_i,y_i\le 2\times 10^3
  • 对于 70%70\% 的数据,xi,yi105x_i,y_i\le 10^5
  • 对于 100%100\% 的数据,xi.yi2311x_i.y_i\le 2^{31}-1k105k\le 10^5

SolutionSolution

显然本题要求逆序对个数,但是如果只是朴素地使用归并排序等做法枚举所有的 x,yx,y 来做,由于 x,yx,y 的范围过大,显然会TLE或者直接爆空间。所以我们要思考如何优化这个求逆序对的过程。

平常我们在遇到值较大但数据量较小的情况时,是不是会考虑离散化处理?本题也是一样的,由于 kk 只到 10510^5 ,而所涉及的所有下标最多为 2k2k ,这个量级是可以接受的。所以我们考虑将出现的位置离散化。

设出现的所有位置的个数为 mm ,则我们将出现的位置按照从小到大的顺序排序,对应的下标为 1,2,3,...,m1,2,3,\,...\,,m 。然后交换的时候,就直接交换对应的下标,最终得到一个新的下标序列,然后对这个下标序列计算逆序对,最终答案的一部分就由这个最终下标序列的逆序对数贡献。

为什么说是一部分?因为我们要知道,对于任意一对相邻的下标,它们之间映射到原位置后是不连续的,意思是对于原位置 pi,pjp_i,p_j ,它们之间还存在 pjpi1p_j-p_i-1 个元素,而这些元素又可能与交换过的位置间形成逆序对。

我们对一个位置 pip_i 进行讨论,设经过交换后, pip_i 被交换到了第 jj 个下标,而原来在这个下标的位置为 pjp_j ,那么映射到原来的位置, pi,pjp_i,p_j 之间有多少个位置?是不是就是 pjpi1|p_j-p_i|-1 个?但由于我们已经计算过交换后下标序列的逆序数,所以如果只用这个数量来计算,由于下标 i,ji,j 之间可能也有其他位置,所以会导致重复计算。所以我们要去掉这些“在交换操作中出现过的位置”,也就是 ji1|j-i|-1 ,所以最终的结果就是 pjpi1ji+1=pjpiji|p_j-p_i|-1-|j-i|+1=|p_j-p_i|-|j-i| ,其中 jj 即为当前交换完成后的下标, ii 是原来的下标,可以用一个vector维护。

CodingCoding

归并排序

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;

整体时间复杂度:O(klogk)O(klogk) ,主要在排序和每次查找位置上。

树状数组

同样的,对于求逆序对,我们还可以使用树状数组,这个应该怎么处理呢?

考虑一个序列 aa ,重新回想一下逆序对的定义,就是那些 i<ji<jai>aja_i>a_j 的数对,则我们先确定位置 ii ,在它的右边能与它产生逆序对的都是比它小的数,那我们从右往左推,每次统计在当前位置 ii 的右边比它小的数的个数,最后累加起来即为答案。

这个过程怎么维护?在数轴上看一下,从 11aia_i ,其中满足条件的数就是 1,2,3,...,ai11,2,3,\,...\,,a_i-1 这些数出现的次数之和。那么显然这是满足前缀性质的,属于一个前缀和计数问题。

那么思路就很显然了,这一块是树状数组的专长,我们维护一个树状数组,每个位置存储这个值出现的次数,然后每次查询 query(ai1)query(a_i-1) 即它的前缀和。但由于位置太大我们无法直接开那么大的数组,所以此时还是要用到离散化,将原位置映射为一串连续的下标,通过下标大小的比较来得出原来位置的大小关系。

CodingCoding

树状数组类

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);
}

同样的,总体时间复杂度为 O(nlogn)O(nlogn) ,主要在排序以及树状数组的更新及查询。

几乎没啥差别:

image.png