[JZOJ5405]【NOIP2017提高A组模拟10.10】Permutation

本文介绍一种通过特定操作使排列达到最小字典序的方法。该算法允许在满足一定条件的情况下交换排列中相隔至少K个位置且数值相差1的元素,以实现最小字典序的目标。文章详细阐述了算法思路及其实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Description

你有一个长度为n 的排列P 与一个正整数K
你可以进行如下操作若干次使得排列的字典序尽量小
对于两个满足|i-j|>=K 且|Pi-Pj| = 1 的下标i 与j,交换Pi 与Pj

对于100% 的数据满足n <= 500000

Solution

隔着K个数之间交换比较麻烦,不如变换一下题意

原序列记录每个位置上的数什么
不妨新开一个数组Q记录每个数在哪个位置。

交换只是相邻两个交换,只要相邻两个数的差的绝对值大于等于K就可以交换。
排列P的字典序最小,与数组Q的字典序最小是等价的。

有了这个结论,就可以直接在数组Q上做了。

下面的值都是数组Q上的值
显然,如果|xy|<K,那么x,y的相对位置无论怎么换,x这个数总是在y前面的。

那么就有了一种N2的做法,对于每一对|Q[i]Q[j]|<K,i<j,把Q[i]Q[j]连边,构成一个DAG,那么构造出来的最小字典序的拓扑序就是最后换出来的Q,再还原成P即可。

最小字典序的拓扑序可以维护一个堆,每次取堆顶加入拓扑序,扩展节点,新走出的节点入度为0时加入堆即可。

然而可以发现,这样连边会有大量不必要的边。
对于Q[x],Q[y],Q[z],x<y<z
如果|Q[x]Q[y]|<K,|Q[y]Q[z]|<K,|Q[x]Q[z]|<K

那么只需要Q[x]连Q[y],Q[y]连Q[z]即可。

那么从后往前扫,对于当前扫到的Q[i],只需要找到i后面最小的x,y,使得
Q[x][Q[i]K+1,Q[i]1],Q[y][Q[i]+1,Q[i]+K1]

Q[i]分别向Q[x],Q[y]连边即可

这个东西可以维护一个权值线段树,从后向前扫的时候插入即可。

Code

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <iostream>
#include <cstdlib>
#include <queue>
#define fo(i,a,b) for(int i=a;i<=b;i++)
#define fod(i,a,b) for(int i=a;i>=b;i--)
#define N 500005
#define INF 2147483647
using namespace std;
int n,w[N],m,m1,n1,fs[N],nt[2*N],dt[2*N],rd[N],d[N],as[N],a1[N];
priority_queue <int> a;
void link(int x,int y)
{
    nt[++m1]=fs[x];
    rd[dt[fs[x]=m1]=y]++;
}
struct node
{
    int s,l,r,s1;
}tr[5*N];
void up(int k)
{
    tr[k].s=min(tr[tr[k].l].s,tr[tr[k].r].s);
    tr[k].s1=max(tr[tr[k].l].s1,tr[tr[k].r].s1);
}
void ins(int k,int l,int r,int x,int v)
{
    if(l==r) tr[k].s=v,tr[k].s1=v;
    else
    {
        int mid=(l+r)/2;
        if(!tr[k].l) tr[k].l=++n1,tr[n1].s=INF;
        if(!tr[k].r) tr[k].r=++n1,tr[n1].s=INF;
        if(x<=mid) ins(tr[k].l,l,mid,x,v);
        else ins(tr[k].r,mid+1,r,x,v);
        up(k);
    }
}
int fd(int k,int l,int r,int x,int y)
{
    x=max(x,l),y=min(y,r);
    if(x>y||k==0) return INF;
    if(l==x&&r==y) return tr[k].s;
    int mid=(l+r)/2;
    return min(fd(tr[k].l,l,mid,x,y),fd(tr[k].r,mid+1,r,x,y));
}
int fd2(int k,int l,int r,int x,int y)
{
    x=max(x,l),y=min(y,r);
    if(x>y||k==0) return 0;
    if(l==x&&r==y) return tr[k].s1;
    int mid=(l+r)/2;
    return max(fd2(tr[k].l,l,mid,x,y),fd2(tr[k].r,mid+1,r,x,y));
}
void doit()
{
    int l=0,r=d[0];
    fo(i,1,n) if(rd[i]==0) a.push(-i);
    while(r<n)
    {
        int k=-a.top();
        a.pop();
        d[++r]=k;
        for(int i=fs[k];i;i=nt[i])
        {
            int p=dt[i];
            rd[p]--;
            if(rd[p]==0) a.push(-p);
        }
    }
}
int main()
{
    cin>>n>>m;
    n1=1;
    fo(i,1,n)
    {
        scanf("%d",&a1[i]);
        w[a1[i]]=i;
    }
    ins(1,1,n,w[n],n);
    fod(i,n-1,1)
    {
        int p=fd(1,1,n,w[i],w[i]+m-1),q=fd(1,1,n,w[i]-m+1,w[i]);
        if(p!=INF) link(w[i],w[p]);
        if(q!=INF) link(w[i],w[q]);
        ins(1,1,n,w[i],i);
    }
    doit();
    fo(i,1,n) as[d[i]]=i;
    fo(i,1,n) printf("%d\n",as[i]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值