8.30 广泛交换意见

题意

给一个长度为\(N\)的排列\(P\)与一个正整数\(K\),可以进行如下操作:

对于两个满足\(|i-j|\geq K\)\(|P_i-P_j|=1\)的下标\(i\)\(j\),交换\(P_i\)\(P_j\)

我们的目的是要求操作后的排列字典序最小

解法

首先,这两个条件都不好判断,直接做是不太好搞的

考虑把这个数组映射一下(这好像是对于排列的一个常见套路?)

\(pos[P_i]=i\),即元素\(P_i\)的位置为\(i\)

我们对\(pos\)数组进行操作,操作就转化为了:

可以交换两个相邻元素,并且相邻元素的差的绝对值\(\geq K\)

我们发现,对于两个元素\(i,j,i<j\),如果\(|i-j|<K\),那么它们的相对位置永远不会改变

也就是说无论进行多少次交换,如何交换,\(i\)一定会在\(j\)之前

这就启发我们用拓扑排序进行求解

但是我们应按照一种什么样的规则选择求出的拓扑序,使得原排列的字典序最小呢?

网上有许多题解都认为,原序列的字典序最小等价于求逆序列的字典序最小

但实际上这个想法是错误的,可以举一组反例来说明

  • \(rev\{2.3.1\} = \{3.1.2\}\)

  • \(rev\{3.1.2\} = \{2.3.1\}\)

我们可以发现,第一个排列的字典序比第二个大,但其逆序列的字典序却比第二个小

实际上,真正正确的结论是原序列的字典序最小,等价于其逆序列的反序列的字典序最大

那么接下来的任务就是建图了

为了避免重复连边,我们规定一个元素仅能向其后面的元素连边

考虑连边的意义,若存在一条有向边\(E(u,v)\),说明\(u,v\)之间的相对顺序已经确定即\(v\)\(u\)之后

对于一个元素\(x\),它会向值域为\((x-K,x)\cup (x,x+K)\)的元素连边,并且要求这些元素的初始位置在\(x\)之后

但是这样暴力连边,边的数量是\(O(N^2)\)级别的

我们可以发现,相对顺序是有传递关系的,比如说对于三条边\(A \rightarrow B, B\rightarrow C,A\rightarrow C\),第三条边完全可以被前两条边替代,那么它就是无效的

为了去掉这些无效的边,我们规定\(x\)只向值域为\((x-K,x)\)\((x,x+K)\)中的位置在\(x\)之后且最小的元素连边

这是因为我们可以发现由于这两个集合的大小均不超过\(K\),那么它们内部一定是互有连边的

那么我们只需要连一个位置最靠前的元素,就可以根据这个元素扩张到整个集合了

由于是求逆序列的反序列最大,我们需要建反图,并用大根堆求字典序最大的序列

找到这个元素可以用线段树实现,由于合法的元素一定在当前元素之后,所以倒序加点即可

代码

#include <queue>
#include <cstdio>
#include <cctype>
#include <cstring>

using namespace std;

const int N = 5e5 + 10;

int read();

int n, k;
int p[N], q[N];

int cap;
int head[N], deg[N], to[N << 1], nxt[N << 1];

priority_queue<int> que;

inline int min(int x, int y) {
    return x < y ? x : y;   
}

inline int max(int x, int y) {
    return x > y ? x : y;   
}

struct SegTree {
    
    int val[N << 2];
    
    void init() {
        memset(val, 0x3f, sizeof val); 
    }
    void change(int x, int l, int r, int k, int v) {
        if (l == r) return val[x] = v, void();
        int mid = l + r >> 1;
        if (k <= mid)   
            change(x << 1, l, mid, k, v);
        else
            change(x << 1 | 1, mid + 1, r, k, v);
        val[x] = min(val[x << 1],  val[x << 1 | 1]); 
    }
    int query(int x, int l, int r, int ls, int rs) {
        if (ls <= l && r <= rs) return val[x];
        int mid = l + r >> 1, res = 0x3f3f3f3f;
        if (ls <= mid)  
            res = min(res, query(x << 1, l, mid, ls, rs));
        if (rs > mid)
            res = min(res, query(x << 1 | 1, mid + 1, r, ls, rs));
        return res; 
    }
} tr;

inline void add(int x, int y) {
    to[++cap] = y, nxt[cap] = head[x], head[x] = cap;   
    ++deg[y];
}

int main() {
    
    n = read(), k = read();
    for (int i = 1; i <= n; ++i)    p[i] = read();
    for (int i = 1; i <= n; ++i)    q[p[i]] = i;
    
    tr.init();
    for (int i = n; i >= 1; --i) {
        int x = tr.query(1, 1, n, q[i], min(q[i] + k - 1, n));
        if (x <= n)
            add(q[x], q[i]);
        int y = tr.query(1, 1, n, max(q[i] - k + 1, 1), q[i]);
        if (y <= n)
            add(q[y], q[i]);
        tr.change(1, 1, n, q[i], i);
    }
    
    for (int i = 1; i <= n; ++i)
        if (!deg[i])    que.push(i);
    for (int i = n; i >= 1; --i) {
        int u = que.top(); que.pop();
        p[i] = u;
        for (int i = head[u]; i; i = nxt[i])
            if (!--deg[to[i]])  que.push(to[i]);
    }
    
    for (int i = 1; i <= n; ++i)    q[p[i]] = i;
    for (int i = 1; i <= n; ++i)    printf("%d\n", q[i]);
    
    return 0;
}

int read() {
    int x = 0, c = getchar();
    while (!isdigit(c)) c = getchar();
    while (isdigit(c))  x = x * 10 + c - 48, c = getchar();
    return x;   
}

转载于:https://www.cnblogs.com/VeniVidiVici/p/11437438.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值