1. 程式人生 > 實用技巧 >【洛谷5470】[NOI2019] 序列(模擬費用流)

【洛谷5470】[NOI2019] 序列(模擬費用流)

點此看題面

大致題意: 給定兩個長度為\(n\)的整數序列,要求在兩個序列中分別選出\(k\)個數,其中至少有\(l\)對數下標相同,使得數的總和最大。

費用流

模擬費用流,自然就是在費用流的基礎上模擬

因此,我們首先要知道如何暴力費用流。

建圖如下:

解釋:一般的邊對應選出一對下標相同的數的情況。由於能任選\(k-l\)對,因此任選邊\(p\rightarrow p'\)的流量是\(k-l\)

然而直接跑費用流顯然是無法通過此題的。

模擬費用流

模擬費用流是對費用流的模擬,接下來的每種情況其實都在費用流的圖中有著對應的流法。

考慮\(p\rightarrow p'\)肯定是最優的邊,因此我們先貪心將其流滿。

具體地,我們對兩個序列各自開一個堆,每次選出還未選擇過的\(a\)中最大元素\(a_u\)\(b\)中最大元素\(b_v\),將總流量\(k\)以及\(p\rightarrow p'\)的容量減\(1\)

\(S\rightarrow S'\rightarrow u\rightarrow p\rightarrow p'\rightarrow v'\rightarrow T\)

然而如果\(b_u\)或者\(a_v\)已經被選擇過了,那麼我們可以讓它們不流\(p\rightarrow p'\)這條任選邊,而是從一般的邊上走,此時可以將\(p\rightarrow p'\)的容量加\(1\)

\(S\rightarrow S'\rightarrow u\rightarrow u'\rightarrow T\)\(S\rightarrow S'\rightarrow v\rightarrow v'\rightarrow T\)

流滿了\(p\rightarrow p'\),接下來我們有\(3\)種選擇:

  • 選擇一對都未選過的\(a_x,b_x\)。(\(S\rightarrow S'\rightarrow x\rightarrow x'\rightarrow T\)
  • 選擇一個最大的\(a_u\),以及一個\(a_v\)已經被選過的\(b_v\)。相當於將原先與\(a_v\)
    通過任選邊連線的\(b\)轉送給\(a_u\),而\(a_v\)\(b_v\)自己走一般邊(但要注意如果\(b_u\)也已經被選過了,需要將\(p\rightarrow p'\)容量加\(1\)重新執行一開始的操作)。(\(S\rightarrow S'\rightarrow u\rightarrow p\leftarrow v\rightarrow v'\rightarrow T\),其中左箭頭表示走反向邊,即退流操作
  • 選擇一個最大的\(b_v\),以及一個\(b_u\)已經被選過的\(a_u\)。同上。

具體實現也就是開五個堆。。。(其中兩個可以延續之前的)

程式碼

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
using namespace std;
int n,k,l,a[N+5],b[N+5],p[N+5],q[N+5];struct Data
{
	int p,v;I Data(CI x=0,CI y=0):p(x),v(y){}
	I bool operator < (Con Data& o) Con {return v<o.v;}
};priority_queue<Data> A,B,A_,B_,C;
//A,B儲存兩個序列中所有未選的元素;A_,B_儲存未選但對應元素已被選的元素;C儲存下標相同都未被選的元素對
class FastIO
{
	private:
		#define FS 100000
		#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
		#define D isdigit(c=tc())
		char c,*A,*B,FI[FS];
	public:
		I FastIO() {A=B=FI;}
		Tp I void read(Ty& x) {x=0;W(!D);W(x=(x<<3)+(x<<1)+(c&15),D);}
}F;
int main()
{
	RI Tt,i,t,x,y,z;long long ans;Data u,v;F.read(Tt);W(Tt--)
	{
		//為了使程式碼更為簡潔,使用了大量#define,請見諒
		#define PA(x) (p[x]=1,q[x]?1:(B_.push(Data(x,b[x])),0))//選擇a[x],打上標記,判斷b[x]是否已經選擇,沒有還要放到堆B_中
		#define PB(x) (q[x]=1,p[x]?1:(A_.push(Data(x,a[x])),0))//選擇b[x],打上標記,判斷a[x]是否已經選擇,沒有還要放到堆A_中
		#define G(g,H) (g=H.top(),H.pop())//取出H的堆頂存放到g中
		#define T(H) (H.empty()?-1e9:H.top().v)//詢問堆頂的值,堆為空時返回-INF
		W(!A.empty()) A.pop();W(!B.empty()) B.pop();//清空
		W(!A_.empty()) A_.pop();W(!B_.empty()) B_.pop();W(!C.empty()) C.pop();//清空
		F.read(n),F.read(k),F.read(l),t=k-l;//t記錄任選邊容量
		for(i=1;i<=n;++i) F.read(a[i]),A.push(Data(i,a[i])),p[i]=0;//讀入並初始化堆A
		for(i=1;i<=n;++i) F.read(b[i]),B.push(Data(i,b[i])),q[i]=0;//讀入並初始化堆B
		ans=0;W(k&&t&&!A.empty()) --k,--t,G(u,A),G(v,B),ans+=u.v+v.v,PA(u.p)&&++t,PB(v.p)&&++t;//先將任選邊流滿,若對應元素已選擇則將容量加1
		for(i=1;i<=n;++i) !p[i]&&!q[i]&&(C.push(Data(i,a[i]+b[i])),0);//將都未被選的元素堆放入堆中
		W(k)
		{
			W(!C.empty()&&(p[C.top().p]||q[C.top().p])) C.pop();//彈去不合法元素
			W(!A.empty()&&p[A.top().p]) A.pop();W(!A_.empty()&&p[A_.top().p]) A_.pop();//彈去不合法元素
			W(!B.empty()&&q[B.top().p]) B.pop();W(!B_.empty()&&q[B_.top().p]) B_.pop();//彈去不合法元素
			--k,x=T(C),y=T(A)+T(B_),z=T(A_)+T(B);//三種情況
			if(x>=y&&x>=z) {G(u,C),ans+=x,p[u.p]=q[u.p]=1;continue;}//選中元素對
			y>=z?(G(u,A),G(v,B_),ans+=y,q[v.p]=1,PA(u.p)&&++t)//注意若b[u]已選擇則將任選邊容量加1
			:(G(u,A_),G(v,B),ans+=z,p[u.p]=1,PB(v.p)&&++t);//注意若a[v]已選擇則將任選邊容量加1
			W(k&&t)//與之前的操作過程一致
			{
				W(!A.empty()&&p[A.top().p]) A.pop();W(!B.empty()&&q[B.top().p]) B.pop();//彈去不合法元素
				--k,--t,G(u,A),G(v,B),ans+=u.v+v.v,PA(u.p)&&++t,PB(v.p)&&++t;//可從上面直接複製下來
			}
		}printf("%lld\n",ans);
	}return 0;
}