链接:
题意:
给一个数组a[],求一个a[]的排列b[],使得任意a[i] != b[i]且b[]的字典序最小。如果不可行输出Impossible
思路:
朴素的暴力解法肯定是在每一位按从小到大的顺序尝试填入每一个数,碰到某一位不可行之后回溯。直到填完,或者某一位无数可用为止(无解)。赛时和zyj讨论这题的可行非暴力解法,相对于暴力解法,关键在于如何避免大量的回溯,由于填数至少要对1->n遍历一次,复杂度已经有O(n),1e5的规模,避免回溯的解法复杂度大概为O(nlogn)级别才不至于TLE,也就是,填入某一个数之后,要在至多O(logn)复杂度内判断这个数可不可行。
一个数可不可行光看自己是不是!=a[i]是不可以的,第i位填入一个数之后,对于每个数,都应满足,i+1和n之间的(闭区间)这n-i位数可以将还未被填入的数字都填进去。也就是探讨"某个数在a[i+1~n]的数量+某个数还未被填入的数量"与n-i的关系。如果<=n-i,则这个数可以被填入,不会导致无解。所有数不会导致无解,在某位填入一个数即可行。"某个数在i+1~n之间有多少个"可以通过"总数-在1~i之间有多少个"来求,某个数未被填入的数量则可以通过"总数-已被填入的数量"来求。
基本思路有了,顺一下,首先枚举每一位(O(n)),然后枚举要往里面填的数x(由于很快遇到可行解,一般来讲并不会到O(n)),然后判断这一位填了x之后,序列出现的每一个数是否满足上述条件(O(n))。发现复杂度还是很高,虽然避免了回溯,但是判断一个数是否满足条件的复杂度还是过高。
考虑对于两个数x,y,当"x在a[i+1~n]的数量+x还未被填入的数量" >= "y在a[i+1~n]的数量+y还未被填入的数量"时,其实是没必要再去判断y可不可行的,因此,用优先队列维护每个数"在a序列中已出现的数量+在b序列中已出现的数量",只要最大的满足条件,小的就不用再去判断。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;
typedef unsigned long long ull;
static const int maxn = 100010;
static const int INF = 0x3f3f3f3f;
static const int mod = (int)1e9 + 7;
static const double eps = 1e-6;
static const double pi = acos(-1);
void redirect(){
#ifdef LOCAL
freopen("test.txt","r",stdin);
#endif
}
int a[maxn],cnta[maxn],cntb[maxn],appear[maxn];
vector<int>v;
set<int>s;
priority_queue<P>pq;
int main(){
redirect();
int T,n;
scanf("%d",&T) ;
while(T--){
v.clear();
s.clear();
while(!pq.empty())pq.pop();
memset(cnta,0,sizeof(cnta));
memset(cntb,0,sizeof(cntb));
memset(appear,0,sizeof(appear));
scanf("%d",&n);
for(int i = 1;i <= n;i++){
scanf("%d",&a[i]);
s.insert(a[i]);
cnta[a[i]]++;
cntb[a[i]]++;
}
for(auto& x : s)pq.push(make_pair(2*cnta[x],x));
for(int i = 1;i <= n;i++){
bool flag = false;
for(auto& x : s){
int& cnt = cnta[x];
if(x == a[i] || cnt-1 + cntb[x] > n-i)continue;
while(1){
P p = pq.top();
pq.pop();
p.first -= appear[p.second];
pq.push(p);
if(appear[p.second] == 0){
if(p.first - (p.second==a[i] || p.second==x) <= n-i){
flag = true;
v.push_back(x);
appear[x]++;
if(--cnt == 0)s.erase(x);
}
break;
}
appear[p.second] = 0;
}
if(flag)break;
}
if(flag){
cntb[a[i]]--;
appear[a[i]]++;
}
else break;
}
if(v.size() == n)for(int i = 0;i < n;i++)printf("%d%c",v[i],i==n-1?'\n':' ');
else puts("Impossible");
}
return 0;
}
另外,wzl说这题可以线段树来做,维护一下一个数在当前位置之后出现的次数和还未匹配的次数加起来。线段树维护,然后全部数这个属性最大的那个如果超过剩下的区间长度,就匹配失败。wzl还是强啊,万物皆可线段树+二分。