[NOI2019] 序列
题面
得分情况
100pts: 6人
>=84pts: 11人
>=64pts: 23人
题解
20pts: \(n\leq 18\)
暴力枚举选哪\(L\)个作为都选的,剩余的\(K-L\)个贪心选取,用set即可。
28pts: \(n\leq 30\)
DP,记录第一个数组选的个数,第二个数组选的个数,和公共的个数,进行转移。
时间复杂度:\(O(n^4)\)
40pts idea1: \(n\leq 150\)
将所有下标按照\(a_i+b_i\)排序。
枚举第\(L\)个公共下标的位置\(x\),我们有如下结论:
对于前\(x\)个下标,最优解不存在一个下标在两个数组中都没有选。
结论显然,若存在,完全可以把一个公共下标放在这个位置,答案会更优。
于是一定存在一个\(y\),使得前\(x\)个位置中\(a\)恰好有\(y\)个没选,\(b\)恰好有\(x-L-y\)个没选。后\(n-x\)个位置中\(a\)恰好有\(K-(x-y)\)个选了,\(b\)恰好有\(K-L-y\)个选了。
那么我们就可以dp求出每个前缀恰好\(a\)有\(u\)个没选,\(b\)有\(v\)个没选的答案,以及后缀\(a\)有\(u\)个选了\(b\)有\(v\)个选了的答案。
实际上枚举的这个\(x\)时一个公共位置的上界。
时间复杂度:\(O(n^3)\)
64pts idea1: \(n\leq 2000\)
在上述40pts的做法下继续思考。
存在一个\(y\),使得前\(x\)个位置中\(a\)恰好有\(y\)个没选,\(b\)恰好有\(x-L-y\)个没选。后\(n-x\)个位置中\(a\)恰好有\(K-x+y\)个位置选了,\(b\)恰好有\(K-L-y\)个位置选了。
有一种贪心的想法:选\(a\)的时候,前面最小的\(y\)个不选,后面最大的\(K-x+y\)个加上;选\(b\)的时候同理,前面最小的\(x-L-y\)个不选,后面最大的\(K-L-y\)个加上。
显然有个小问题:\(a\)和\(b\)可能同时删掉了某个下标,这样前\(x\)个位置就有大于\(L\)个公共的了。
但是,如果\(a\)和\(b\)同时没选某个下标,这个方案会比所有\(a\)和\(b\)没有同时没选某个下标的方案优,且这个方案显然不是最优,那么最终答案的\(x\)肯定不是现在的枚举的这个\(x\)。
于是预处理出前缀和后缀各自的前\(x\)大/小和,贪心的做即可。
时间复杂度:\(O(n^2)\)
84pts idea1:\(\sum n\leq 3e5\)
选\(a\)前面最小的\(y\)个删掉,后面最大的\(K-x+y\)个加上,\(b\)选前面最小的\(x-L-y\)个删掉,后面最大的\(K-L-y\)个加上。
我们设\(a0\)为\(a\)前面\(x\)个元素的集合,\(a1\)为\(a\)后面\(n-x\)个元素的集合,\(b0,b1\)同理。
一个不同的思考方式是,进行\(K-L\)次操作。我们每次可以:
- 将\(a0\)的最小元素删掉,\(a1\)的最大元素加上。
- 将\(b0\)的最小元素删掉,\(b1\)的最大元素加上。
我们可以用数据结构对任意\(u\)次计算出第\(u\)次操作\(1\)或操作\(2\)的增量。
不妨假设最后一次我们进行的是操作\(1\),那么这意味着进行一次操作\(1\)的增量比操作\(2\)的增量要大,反之亦然。
于是,假设进行了\(y\)次操作\(1\),\(K-L-y\)次操作\(2\),那么第\(K-L-y+1\)次操作\(2\)的代价比第\(y\)次操作\(1\)的代价要大。注意到这样的\(y\)是可二分的,也就是说如果第\(y\)次操作\(1\)的代价比第\(K-L-y+1\)次操作\(2\)的代价要小,那么对于所有\(y'>y\),第\(y\)次操作\(1\)的代价也会比第\(K-L-y'+1\)次操作\(2\)的代价要小。
于是可以二分这个\(y\),使用支持加入和查询第\(k\)大的数据结构维护即可做到\(O(nlog^2n)\)。
100pts idea1:
引理:对于\(i\)和\(j\),设\(x=i\)时的最优解(为了方便讨论假设最优解唯一)中,删除的下标集合为\(S_i\),对于\(x=j\)时的下标集合为\(S_j\),则\(S_i\)为\(S_j\)的子集。
证明:
假设\(x=i\)时的最优解存在一个下标\(u\)被删除,但是在\(x=j\)时该下标未被删除。
不妨假设这个下标在\(a\)中,且\(b\)中\(j\)删除的元素个数不比\(i\)删除的元素个数少。
那么我们可以尝试找一个\(v\leq i\),使得\(i\)中\(v\)没有被删除,但是在\(j\)中被删除,然后我们可以在\(j\)中用\(a_v\)替换\(a_u\),这个替换一定不会使解变劣,否则我们可以在\(i\)中进行逆操作使得\(i\)的解变优。
假设找不到这样的\(v\leq i\),这说明在前\(i\)个元素中\(i\)中删除的元素时\(j\)的真子集,我们在\(b\)中尝试找一个下标\(l\leq i\)使得它在\(i\)中没被删除而在\(j\)中被删除,如果这个下标也找不到,那么\(S_i\)时\(S_j\)的子集,这意味着我们\(i\)的方案本身就符合\(j\)的条件,所以\(x=j\)时我们可以直接用\(x=i\)的答案。
我们再找一个下标\(m>i\)使得它在\(i\)中没被选而在\(j\)中被选,根据\(b\)中\(j\)删除的元素个数不比\(i\)删除的元素个数少的假设我们也能证明这个\(m\)一定能找到。
于是我们可以用\(a_v\)替换\(a_u\),\(b_l\)替换\(b_m\),同样的道理可以证明这个替换不会使解变劣。
我们根据引理容易发现这个\(y\)时单调的,而且如果当前枚举的\(y\)小于正确答案的\(y\),那么我们直接将\(y+1\)答案不会变劣(这是因为\(84\)分做法里的单调性)。
所以我们将二分改成双指针就可以将时间复杂度变为\(O(nlogn)\)。
但是我们注意到,由于\(a\)选前面最小的\(y\)个删掉,\(b\)选前面最小的\(x-L-y\)个删掉,如果\(x\)加一,为了保证两个数都不减,\(y\)只能不变或者加一。
也就是说,每一步我们的可能性只有两种:\(y\)加一或者不加。我们可以想一想这个做法是在干什么。
对于\(x=K\),我们将和是前\(L\)大的下标都选上,然后在后面\(n-K\)个下标中,\(a\)和\(b\)都各自选最大的\(K-L\)个。
那么如果\(x\)加一,思考一下\(y\)加一或者不加分别对应哪些操作。
- \(x\)这个下标在\(a\)和\(b\)两个数组中都选了:
操作1:将\(a0\)中的最小值删掉,\(a1\)中没选的最大值加进来。
操作2:将\(b0\)中的最小值删掉,\(b1\)中没选的最大值加进来。 - \(x\)这个下标只在一个数组中选了(不妨假设是\(a\)):
操作1:将\(a0\)中的最小值删掉,\(a1\)中没选的最大值加进来,\(b1\)中选了的最小值删掉。
操作2:将\(b0\)中的最小值删掉。 - \(x\)这个下标在两个中都没选:
操作1:将\(a0\)中的最小值删掉,\(b1\)中选了的最小值删掉。
操作2:将\(b0\)中的最小值删掉,\(a1\)中选了的最小值删掉。
于是使用\(6\)个优先队列即可解决这个问题。
64~84pts idea2:
注意到对于固定的\(L\),答案关于\(K-L\)是凸的,因此可以使用wqs二分。
二分一个权值\(w\),限制公共下标数量\(L\)但不限制\(K\),然后对于除了公共下标外每个多选的下标都给一个\(w\)的额外代价,二分\(w\)使最优解恰好为\(K\)。
固定\(w\)时,可以贪心求解。
40~64pts idea3: \(n\leq 30\rightarrow n\leq 2000\)
考虑费用流模型。
至少\(L\)个下表相同,等价于至多有\(K-L\)个下标不同。
对于每个\(i\):
连边\(S\rightarrow a_i\),容量为\(1\),边权为\(a_i\)。
连边\(b_i\rightarrow T\),容量为\(1\),边权为\(b_i\)。
连边\(a_i\rightarrow b_i\),容量为\(1\),边权为\(0\),表示选择一对相同的下标。
新建点\(U\),\(V\),连边\(U\rightarrow V\),容量为\(K-L\)(称为自由流量),边权为\(0\),表示选择一对不同的下标。
对每个\(i\),连边\(a_i\rightarrow U\),\(V\rightarrow b_i\)。
注意限制总流量为\(K\)。

跑最大费用最大流即可。
100pts idea3:
考虑模拟费用流:
观察算法流程,所有和\(S\),\(T\)直接相连的边不会退流,所以可以忽略这些边的反向边,而中间的边会退流,对应每一对的添加操作。
观察增光路有多少形态:

假设某次选择下标对\((a_i,b_j)\),
其中分别对应以下\(5\)种方案:
1.\(i=j\)。
2.\(i\not=j\),消耗\(1\)自由流量。
3.使\(a_i\)与\(b_i\)(先前已选)配对,将\(b_j\)与\(a_j\)(先前已选)配对,增加\(1\)自由流量。
4.使\(a_i\)与\(b_i\)(先前已选)配对,\(b_j\)与先前与\(b_i\)配对的\(a_x\)配对。
5.使\(b_i\)与\(a_i\)(先前已选)配对,\(a_j\)与先前与\(a_i\)配对的\(b_x\)配对。
(其实图中还存在其他形式的增光路,但显然存在更优的形式,故只考虑这5种即可)
考虑怎么维护以上\(5\)种方案。
可以用五个堆\(q,q_a,q_b,q_{a_2},q_{b_2}\),分别表示:
\(q:\)两边都未选的\(a_i+b_i\)的\(max\)。
\(q_a:\)\(A\)数组未选的数的\(max\)。\(q_b\)同理。
\(q_{a_2}:\)\(a_i\)未选而\(b_i\)已选的\(a_{i_{max}}\)。\(q_{b_2}\)同理。
若某堆顶不存在则视为\(-INF\)。
用两个\(bool\)数组维护某一下标是否被取过。
当取走某一下标时,若另一侧未取,则可以对相应的\(q_{a_2}\)或\(q_{b_2}\)添加元素。
模拟方案的过程,取\(max\)即可。
时间复杂度:\(O(nlogn)\)
具体细节参考代码:
#include<bits/stdc++.h>
#define ll long long
#define uit unsigned int
#define mkp make_pair
#define pa pair
#define fir first
#define sec second
using namespace std;
const int N=2e5+10,INF=1e9;
int T,n,K,L,A[N],B[N];bool visa[N],visb[N];
priority_queue<pa<int,int> > q,qa,qb,qa2,qb2;
void seta(int R){
visa[R]=1;
if(!visb[R]) qb2.push(mkp(B[R],R));
}
void setb(int R){
visb[R]=1;
if(!visa[R]) qa2.push(mkp(A[R],R));
}
void solve(){
scanf("%d%d%d",&n,&K,&L);int lim=K-L,mx=0,op=0;ll ans=0;
for(int i=1;i<=n;i++) scanf("%d",&A[i]);
for(int i=1;i<=n;i++) scanf("%d",&B[i]);
for(int i=1;i<=n;i++) q.push(mkp(A[i]+B[i],i)),qa.push(mkp(A[i],i)),qb.push(mkp(B[i],i));
q.push(mkp(-INF,0));qa.push(mkp(-INF,0));qb.push(mkp(-INF,0));qa2.push(mkp(-INF,0));qb2.push(mkp(-INF,0));
ans=0;
for(int i=1;i<=K;i++){
mx=op=0;
while(visa[q.top().sec]||visb[q.top().sec]) q.pop();
while(visa[qa.top().sec]) qa.pop();
while(visb[qb.top().sec]) qb.pop();
while(visa[qa2.top().sec]) qa2.pop();
while(visb[qb2.top().sec]) qb2.pop();
pa<int,int> qnow=q.top(),qanow=qa.top(),qbnow=qb.top(),qa2now=qa2.top(),qb2now=qb2.top();
mx=qnow.fir;op=1;
if(qa2now.fir+qb2now.fir>mx){
mx=qa2now.fir+qb2now.fir;
op=3;
}
if(qa2now.fir+qbnow.fir>mx){
mx=qa2now.fir+qbnow.fir;
op=4;
}
if(qanow.fir+qb2now.fir>mx){
mx=qanow.fir+qb2now.fir;
op=5;
}
if(qanow.sec!=qbnow.sec&&lim){
if(qanow.fir+qbnow.fir>mx){
mx=qanow.fir+qbnow.fir;
op=2;
}
}
if(op==1){seta(qnow.sec);setb(qnow.sec);}
if(op==2){seta(qanow.sec);setb(qbnow.sec);lim--;}
if(op==3){seta(qa2now.sec);setb(qb2now.sec);lim++;}
if(op==4){seta(qa2now.sec);setb(qbnow.sec);}
if(op==5){seta(qanow.sec);setb(qb2now.sec);}
ans+=mx;
}
printf("%lld\n",ans);
while(!q.empty()) q.pop();
while(!qa.empty()) qa.pop();
while(!qb.empty()) qb.pop();
while(!qa2.empty()) qa2.pop();
while(!qb2.empty()) qb2.pop();
for(int i=1;i<=n;i++) visa[i]=visb[i]=0;
}
int main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
scanf("%d",&T);
while(T--) solve();
return 0;
}

浙公网安备 33010602011771号