1. 程式人生 > >模擬費用流

模擬費用流

方案 強制 printf 貪心 priority 表示 得到 相交 序列

模擬費用流

一些感受

這個東西好神仙啊.jpg

$Orz ??? laofu$

註意事項:本文代碼不保證正確性,帶有頭文件的是正確的

一組套題

給你$n$個老鼠,$m$個洞,求一個滿足要求的匹配的代價。

一個簡單的部分

  • 滿足,洞的容量為$1$,老鼠不能分身,代價為距離,求最小代價。

Pro 1

擁有一個限制:只能向左走。

直接排序即可。

Pro 2

無限制。

一種針對這種問題的$O(n)$解法

顯然可以給出DP方程:$f[i][j]$表示,前$i$個位置,有 $ j $ 個洞需要匹配,其中$j$可以為負,表示的意義為有$-j$個老鼠需要匹配。

顯然,這個一種常見的思路是將每一個距離拆開看,為$|x_i-y_j|$。

由於在匹配過程中,交叉匹配一定會有不比它差的非交叉匹配方案,所以我們將所有的交叉匹配方式去掉。

  • 對於老鼠:$j\ge 0,f[i][j]=f[i-1][j+1]+x[i],j< 0,f[i][j]=f[i-1][j+1]-x[i]$,因為所有老鼠都需要進入洞中,所以沒有決策。
  • 對於洞:$j> 0,f[i][j]=\min ( f[i-1][j-1]-y[i] ,f[i-1][j]),j\le 0,f[i][j]=f[i-1][j-1]+y[i]$顯然,對於後者,因為滿足$y[i]$單調遞增,所以必定為直接轉移最優 。

但是,顯然這樣轉移是沒有辦法優化掉的,是滿的$n^2$,所以我們考慮如何通過分析性質將其優化掉。

  • 對於洞的優化:顯然,對於決策$j>0$時,滿足直接轉移更小,因為$y[i]$一定會比$y[i-1]$大所以可以直接用現在的$y[i]$來替換當時的$y[i-1]$就可以變得更優,但是考慮到所有老鼠需要完全匹配,所每次只需要維護$f[i][0]$的答案即可,其他的必定會直接轉移最優。

現在我們發現,對於上述DP方程,僅有直接覆蓋決策和區間擡升。

簡易代碼如下:

stack sz,sf;
int tag1=0,tag2=0,f0=0;
for(int i=1;i<=n;i++)
{
    if(op[i]==1)//洞
    {
        tag1-=a[i],tag2+=a[i];
        sf.push(f0+a[i]-tag2);
        f0=min(sz.top()+tag1-a[i],f0);sz.pop();
    }else // 鼠
    {
        tag1+=a[i],tag2-=a[i];
        sz.push(f0+a[i]-tag1);
        f0=sf.top()+tag2-a[i];sf.pop();
    }
}
一個更加具有普適性的$O(n\log n)$解法

顯然,對於這種匹配問題,一定不會存在匹配交叉的情況,所以我們考慮在此的基礎上進行貪心。

對於上述DP方程,我們給出如下差分結果:

$\begin{cases} d[i][j]=f[i][j]-f[i][j-1] , j>0 \ d[i][j]=f[i][j]-f[i][j+1],j<0\end{cases}$

那麽給出如下轉移,由於我們發現,通過上述表達,只能表達出$f[i][j]$之間的關系,無法準確的表達出$f[i][j]$,所以我們需要維護一個$f[i][0]$來得到所以的關系。

那麽給出轉移:

  • 對於老鼠:$f[i][0]=f[i-1][0]+d[i-1][1]+a[i],d[i][j]=\begin{cases}d[i-1][j+1] , j>0||j<-1\-d[i-1][1]-x[i]\times 2\end{cases}$

  • 對於洞:$f[i][0]=\min(f[i-1][0],f[i-1][0]+d[i-1][-1]+a[i])$

    拆開看:

    • 如果$d[i-1][-1]+a[i]<0$:$f[i][0]=f[i-1][0]+d[i-1][-1]+a[i],d[i][j]=\begin{cases}d[i-1][j-1],j>1||j<0 \-d[i][-1]-y[i]\times 2 \end{cases}$
    • 否則:$f[i][0]=f[i-1][0],d[i][j]=\begin{cases}d[i-1][j-1],j>1||j<0 \y[i] \end{cases}$

這個東西顯然就可以用上面的那個棧的做法用差分意義理解了...

有這個東西,可以發現這個DP是具有凸性的,也就是說,對於這個DP來說,$d[i][j]$在$j>0$時單調遞增,在$j<0$時單調遞減。

所以直接用堆來維護一下最小的$d[i][j],j>0$和最小的$d[i][j],j<0$即可。

實現代碼如下:

priority_queue<int>q0,q1;
for(int i=1;i<=n;i++)
    if(op[i]==1)//洞
    {
        if(q0.top()+a[i]<0)
        {
            f0=f0+q0.top()+a[i];
            q1.push(-q0.top()-a[i]*2);
            q0.pop();
        }else q1.push(-a[i]);
    }else // 鼠
    {
        f0=f0+q1.top()+a[i];
        q0.push(-q1.top()-a[i]*2);
        q1.pop();
    }

另外的一種分析方式:

對於當前的兩個堆,分別相當於是對於鼠和對於洞的匹配集合,每次強制老鼠匹配一個尚未匹配的,在他左邊的洞,然後可以在之後的操作中將這次匹配反悔,變成匹配之後的某一個洞。

Pro 3

現在,對於給定問題,老鼠只能向左走,並且代價為$ x_i-y_j+w_j $,不一定每個老鼠都進入洞中,求最大代價。

好像這個題可以隨便做的樣子...

直接按照$a_i$從左到右的順序,維護一個對於洞的堆,其中的比較關鍵字是按照$w_j-y_j$從大到小排序。

然後每次取出堆頂進行匹配即可...

一個加強之後的部分

Pro 4

對於每個洞,有一個容量限制,也就是每個洞可以容納$b_i$個老鼠。

  • 一個弱智舉了手:如果$\sum\limits_{i=1}^nb_i\le 10^6$我會!拆開每個洞然後進行Pro 2即可!
  • (看待弱智的眼神

好的,我們接下來考慮這個題如何處理...

一個神仙的做法

只需要強制往左跑,和強制往右跑的構成的堆的交集做一發即可。

這個東西的時間復雜度顯然是$O(n\log n)$的。

對此的證明:顯然我不會。

我所能想到的做法

顯然,只需要在維護堆的時候,傳進兩個參數,分別表示剩余的容量,和相應的代價,然後按照第二維維護最小值即可。

每次把對應容量減去,如果為$0$就不再壓入...

大致代碼:

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <queue>
#include <iostream>
#include <bitset>
using namespace std;
#define N 200005
#define ll long long
const long long inf = 1ll<<40;
struct node{int op,lim;ll x,w;}a[N];
bool cmp(const node &a,const node &b){return a.x<b.x;}
int tot,n,m;ll f0,sum;
priority_queue<pair<ll ,int > ,vector<pair<ll ,int > > ,greater<pair<ll ,int > > >q1;
priority_queue<ll ,vector<ll > ,greater<ll > >q0;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1,x;i<=n;i++)scanf("%d",&x),a[i]=(node){0,0,x,0};tot=n;
    for(int i=1;i<=m;i++)tot++,scanf("%lld%lld%d",&a[tot].x,&a[tot].w,&a[tot].lim),a[tot].op=1,sum+=a[tot].lim;
    a[++tot]=(node){1,n,-inf,0};a[++tot]=(node){1,n,inf,0};
    if(sum<n)return puts("-1"),0;sort(a+1,a+tot+1,cmp);
    for(int i=1;i<=tot;i++)
        if(a[i].op==1)//洞
        {
            int t=a[i].lim;
            while(!q0.empty()&&t&&q0.top()+a[i].x<0)
            {
                f0=f0+q0.top()+a[i].x;
                q1.push(make_pair(-q0.top()-a[i].x*2,1));
                q0.pop();t--;
            }
            if(t)q1.push(make_pair(-a[i].x,t));
        }else // 鼠
        {
            pair<ll ,int > tmp=q1.top();q1.pop();
            f0=f0+tmp.first+a[i].x;
            q0.push(-tmp.first-a[i].x*2);tmp.second--;
            if(tmp.second)q1.push(tmp);
        }
    printf("%lld\n",f0);
}

Pro 5

每個老鼠可以無限分身,但是分出來的身一定要進入洞中,每個洞有一個容量無限,但是至少有一只老鼠。

  • 暴力大家都會(真的嗎?

然後似乎正解也不是很難,我們只需要這樣考慮,對於每個老鼠的權值,分成兩類,一部分,容量為$1$,費用為$-\infty+d[i][1]+x_i\times 2$,一部分容量為$\infty$,費用為$d[i][1]+x[i]\times 2$

然後對於每個洞,也就同樣,當做Pro4中容量上限為$\infty$來做。

然後我們考慮到,兩個容量$\infty$之間匹配,一定不優,所以不存在這種匹配情況。

大致代碼:

priority_queue<pair<int ,int > >q0,q1;
for(int i=1;i<=n;i++)
    if(op[i]==1)//洞
    {
        pair<int ,int >tmp;tmp=q0.top();q0.pop();
        if(tmp.first+a[i]-inf<0)
        {
            f0=f0+tmp.first+a[i];
            q1.push(make_pair(tmp.first,1));tmp.second--;
            if(!tmp.second)tmp=q0.top();
        }else q1.push(make_pair(a[i]-inf,1));
        while(tmp.first+a[i]<0)
        {
            f0=f0+tmp.first+a[i];q0.pop();
            q1.push(make_pair(-tmp.first-a[i]*2,1));tmp.second--;
            if(tmp.second)q0.push(tmp);
        }
        q1.push(a[i],inf);
    }else // 鼠
    {
        pair<int ,int >tmp;tmp=q1.top();q1.pop();
        if(tmp.first+a[i]-inf<0)
        {
            f0=f0+tmp.first+a[i];
            q0.push(make_pair(tmp.first,1));tmp.second--;
            if(!tmp.second)tmp=q1.top();
        }else q0.push(make_pair(a[i]-inf,1));
        while(tmp.first+a[i]<0)
        {
            f0=f0+tmp.first+a[i];q1.pop();
            q0.push(make_pair(-tmp.first-a[i]*2,1));tmp.second--;
            if(tmp.second)q1.push(tmp);
        }
        q0.push(a[i],inf);
    }

pro 6

在最基礎的模型上增加,每個洞有一個權值$w_i$,老鼠進洞的代價變為:$|x_i-y_i|+w_i$

想明白了,也不是很難。

只需要考慮到上述式子在不同情況下,代價不同,分別是$w_i-y_i$和$w_i+y_i$,然後分別作為洞和老鼠的反悔情況。

這樣的意義代表著,老鼠即使匹配了右側的一個洞,也可能會反悔去匹配更右側的一個洞,因為這樣可能代價更小。

其他的東西倒是沒啥區別。

簡易代碼如下:

priority_queue<int>q0,q1;
for(int i=1;i<=n;i++)
    if(op[i]==1)//洞
    {
        if(q0.top()+a[i]+w[i]<0)
        {
            f0=f0+q0.top()+a[i]+w[i];
            q1.push(-q0.top()-a[i]*2);
            q0.pop();
            q0.push(-a[i]-w[i]);
        }else q1.push(-a[i]+w[i]);
    }else // 鼠
    {
        f0=f0+q1.top()+a[i];
        q0.push(-q1.top()-a[i]*2);
        q1.pop();
    }

pro 7

我們可以考慮,如果模型轉化到樹上了,如何解決。

(可能大概這個題可以被一眼秒掉了。

直接把堆換成可並堆,同樣,它也不能交叉匹配,這裏的交叉匹配是指在同一條鏈上時,兩者交叉匹配。

沒有簡易代碼

pro 8 UER8 T2 雪災與外賣

題目鏈接

其實還是最基礎的模型,上面加上了容量和額外代價。

直接把上面Pro 6和Pro 4的做法結合起來即可。

#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <queue>
#include <iostream>
#include <bitset>
using namespace std;
#define N 200005
#define ll long long
const long long inf = 1ll<<40;
struct node{int op,lim;ll x,w;}a[N];
bool cmp(const node &a,const node &b){return a.x<b.x;}
int tot,n,m;ll f0,sum;
priority_queue<pair<ll ,int > ,vector<pair<ll ,int > > ,greater<pair<ll ,int > > >q1,q0;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1,x;i<=n;i++)scanf("%d",&x),a[i]=(node){0,0,x,0};tot=n;
    for(int i=1;i<=m;i++)tot++,scanf("%lld%lld%d",&a[tot].x,&a[tot].w,&a[tot].lim),a[tot].op=1,sum+=a[tot].lim;
    a[++tot]=(node){1,n,-inf,0};a[++tot]=(node){1,n,inf,0};
    if(sum<n)return puts("-1"),0;sort(a+1,a+tot+1,cmp);
    pair<ll ,int > tmp;
    for(int i=1;i<=tot;i++)
        if(a[i].op==1)//洞
        {
            int t=a[i].lim;
            while(!q0.empty()&&t&&q0.top().first+a[i].w+a[i].x<0)
            {
                tmp=q0.top();q0.pop();
                int now=min(t,tmp.second);
                f0=f0+(ll)now*(tmp.first+a[i].x+a[i].w);
                q1.push(make_pair(-tmp.first-a[i].x*2,now));
                t-=now;tmp.second-=now;if(tmp.second)q0.push(tmp);
            }
            if(a[i].lim!=t)q0.push(make_pair(-a[i].x-a[i].w,a[i].lim-t));
            if(t)q1.push(make_pair(-a[i].x+a[i].w,t));
        }else // 鼠
        {
            tmp=q1.top();q1.pop();
            f0=f0+tmp.first+a[i].x;
            q0.push(make_pair(-tmp.first-a[i].x*2,1));tmp.second--;
            if(tmp.second)q1.push(tmp);
        }
    printf("%lld\n",f0);
}

pro 9

沒看懂這題在說些啥

pro 10

給一個序列,要求選出$K$個區間,每個區間不相交,求最大和。

直接線段樹維護區間連續最大值,每次區間取相反數即可。

pro 11

看樣子像是Pro 9 + Pro 10的合體,反正不太可寫...

預估代碼會和蜀道難有一拼了.jpg

pro 12 NOI 2017 D2T2 蔬菜

題目鏈接

暴力的話,費用流直接建圖應該有$40\sim60$分不等,我不知道我自己哪裏寫掛掉了,wa了好幾個點...

由於我們發現,隨著天數的增加,選擇的蔬菜的集合只會變成原先的父集,也就是不會退流,所以直接使用線段樹維護增廣的合法性即可,剩下的就是堆+貪心了。

pro 13

有一棵$n $個點的樹,$m$個人站在樹根。每條邊有一個邊權。現在每個人都可以任意行走,經過一條邊就要付出對應邊權的代價。問最小代價使得每條邊至少被一個人經過。$n \le 10^5,m \le 50$。

直接DP不解釋。

模擬費用流