解析·NOIP·冷門 CLZ最小環

迷失の風之旅人發表於2018-11-05

賜予我力量,去改變我所能改變的;賜予我勇氣,去接受我不能改變的;並賜予我智慧,去分辨這兩者。
—–安東尼達斯

NOIP的圖論題中有一部分是跟圖上的環有關的。蒟蒻的我在USACO上刷題時發現了一種(或許)還沒有普及的快速最小環演算法,擁有極高的效率,可以在求最短路的時間複雜度內求出經過任意一點的最小環大小或權值。作者將它稱作Calculate lacework zoomly shortest cyclic,這裡暫譯作CLZ最小環
與Floyd求圖上最小環不同,CLZ最小環還可以很快捷地求出經過特定點的最小環。
CLZ最小環的思路也極為簡單,很容易理解:對於求經過一個點(s)的最小環時,即首先進行最短路操作,在進行第一次鬆弛操作後重新將該點標記為未訪問,並重新訪問。由於是與最短路同時運作,所以其複雜度幾乎取決於最短路演算法。當然,CLZ最小環仍有其獨立的優化方案。

樸素情況

首先讓我們考慮樸素情況,非負帶環有向圖。對於求最小環中所含的點數,我們只需要將每一條邊的權值修改為1,再按上述過程操作即可。
至於如何回退操作,相信不用細講。我們可以在最短路演算法的最前定義一個布林型變數bool wait初始值為1,在進行完鬆弛操作後判斷wait的狀態來決定是否需要將起點重新放入。

void dijkstra(int u){   
    bool wait=1;
    /*
    blblblblblbl
    */
    if(wait){
       dis[u]=0x3f3f3f3f,vis[u]=0;//對單個點進行初始化
       q.push(u);//將點重新放入
       wait=0;//設定當前鬆弛次數
    }
}

函式程式碼如下

void dijkstra(int u){
    pall note;
    bool wait=1;
    for(register int i=0;i<maxn;i++)dis[i]=0x3f3f3f3f,vis[i]=0;
    dis[u]=0;vis[u]=1;
    set<pall,cmp> s;
    s.insert(make_pair(u,0));
    for(register int i=0;i<n && s.size();i++){
        set<pall,cmp>::iterator it=s.begin();
        u=it->X;
        s.erase(*it);
        vis[u]=1;
        for(register int j=p[u];~j;j=E[j].next){
            int v=E[j].v;
            int w=E[j].w;
            if(dis[v]>dis[u]+w && !vis[v]){
                s.erase(make_pair(v,dis[v]));
                s.insert(make_pair(v,dis[v]=dis[u]+w));
                note=make_pair(u,v);
            }
        }
        if(wait){
            dis[u]=0x3f3f3f3f;
            vis[u]=0;
            wait=0;
        }
    }
}

無向圖

對於無向圖來說,若允許經過同一條邊多次,則與上述操作無異。但若每條邊只能經過一次,則需要一些額外的操作。因為無向圖的性質,我們可以知道,從(u)(v)的路徑權值和與從(v)(u)的路徑權值和是相等的。因此我們可以考慮判斷當前路徑的權值(w)與上一條路徑的權值(w)是否相等,並以此來考慮是否選擇當前路徑。
當然也有更簡單的判斷方式。若資料量較大,我們只需將等待的wait往後延長一次鬆弛操作即可。而資料較小時,我們可以直接暴力求解,不在本題的考慮範圍之內。
對於普適且較為常規的情況,我們可以先求出點(S)連向的邊與(S)的距離,儲存該距離,並列舉每一個距離的總長度即可。

if(dis[u]==w)continue;
/*-----------寫法分割線------------*/
if(wait==1 && E[i ^ 1].w==w)continue;
wait++;
int wait=0;
if(wait==1){
    dis[u]=0x3f3f3f3f;
    q.push(u);
    //wait=0;
}

帶負權

負環判斷

普通的spfa可以直接解決,也可以將所有邊權去反,再判斷帶負權的正環。詳情見下文詳述。

正環判斷

所謂正環有兩種定義。一個環上所有邊為正,或一個環上權值的和為正。
對於第一種定義,我們只需要在遇見負邊時跳過即可。
對於第二種定義,則需要些許變通。我們不再直接修改或操作原邊權值,而是再在結構體中宣告新的變數將點的數量轉化為邊權值。如下圖,兩種邊權是不會互相干擾的。
解析·NOIP·冷門 CLZ最小環
由於有負邊權,我們選擇使用spfa跑最短路。我們在跑最短路的過程中同時計算兩種權值。當且僅當兩種邊權都有更優解時我們才用新答案替換原有答案(可以稍微思考一下)。當我們按題目給的邊權走,使得當前最短路為負數時,我們直接跳過該判斷。並不用但擔心這裡會跳出正確答案,因為由於我們會在每一個點都進行一次spfa尋找操作,所以有且一定有至少一種情況讓我們在跑環的過程中邊權絕不為負。且這裡的為負就跳出也為CLZ最小環演算法提供了很好的剪枝。
以下是帶負邊權的有向圖最小環程式碼

void spfa(int u){
    //額外的,E[i].a記錄點的數量轉化來的邊權,即E[i].a恆等於1 
    //ges[u]記錄當前最小環內的點數,即a權值維護的最小值 
    bool wait=1;
    for(register int i=0;i<maxn;i++)dis[i]=0x3f3f3f3f,vis[i]=0,ges[i]=0x3f3f3f3f;
    dis[u]=0;vis[u]=1;
    ges[u]=0;
    queue<int> q;
    q.push(u);
    while(!q.empty()){
        u=q.front();
        q.pop();
        vis[u]=0;
        for(register int i=p[u];~i;i=E[i].next){
            int v=E[i].v;
            int w=E[i].w;
            int a=E[i].a;
            if(dis[v]>dis[u]+w && ges[v]>ges[u]+a && dis[u]+w>0){
                ges[v]=ges[u]+a;
                dis[v]=dis[u]+w;
                //cout<<dis[v]<<" "<<dis[u]+w<<endl;
                if(!vis[v]){
                    vis[v]=1;
                    q.push(v);
                }
            }
        }
        if(wait){
            q.push(u);
            dis[u]=0x3f3f3f3f;
            vis[u]=0;
            ges[u]=0x3f3f3f3f;
            wait=0;
        }
    }
}

我們可以發現,其實CLZ最小環只是一種延遲操作的思想。作者只不過是使用了這一思路的一小部分,利用其智慧將最短路的鬆弛操作分開而已。或許在未來作者會以該思路獲得更多靈感,筆者也會持續關注。
最後附上樸素寫法的板子

#include<bits/stdc++.h>
#define maxn 3000
#define maxm 5000
#define X first
#define Y second
#define pall pair<int,int>
using namespace std;
inline char get(){
    static char buf[300],*p1=buf,*p2=buf;
    return p1==p2 && (p2=(p1=buf)+fread(buf,1,30,stdin),p1==p2)?EOF:*p1++;
}
inline int read(){
    register char c=get();register int f=1,_=0;
    while(c>`9` || c<`0`)f=(c==`-`)?-1:1,c=get();
    while(c<=`9` && c>=`0`)_=(_<<3)+(_<<1)+(c^48),c=get();
    return _*f;
}
struct edge{
    int u,v,w,next;
}E[maxm];
struct cmp{
    operator ()(const pall &a,const pall &b)const{
        if(a.Y!=b.Y)return a.Y<b.Y;
        return a.X<b.X;
    }
};
int p[maxn],eid;
inline void init(){
    for(register int i=0;i<maxn;i++)p[i]=-1;
    eid=0;
}
inline void insert(int u,int v,int w){
    E[eid].u=u;
    E[eid].v=v;
    E[eid].w=w;
    E[eid].next=p[u];
    p[u]=eid++;
}
inline void insert2(int u,int v,int w){
    insert(u,v,w);
    insert(v,u,w);
}
int n,m;
int dis[maxn],vis[maxn];
void dijkstra(int u){
    pall note;
    bool wait=1;
    for(register int i=0;i<maxn;i++)dis[i]=0x3f3f3f3f,vis[i]=0;
    dis[u]=0;vis[u]=1;
    set<pall,cmp> s;
    s.insert(make_pair(u,0));
    for(register int i=0;i<n && s.size();i++){
        set<pall,cmp>::iterator it=s.begin();
        u=it->X;
        s.erase(*it);
        vis[u]=1;
        for(register int j=p[u];~j;j=E[j].next){
            int v=E[j].v;
            int w=E[j].w;
            if(dis[v]>dis[u]+w && !vis[v]){
                s.erase(make_pair(v,dis[v]));
                s.insert(make_pair(v,dis[v]=dis[u]+w));
                note=make_pair(u,v);
            }
        }
        if(wait){
            dis[u]=0x3f3f3f3f;
            vis[u]=0;
            wait=0;
        }
    }
}
int u,v,w;
int main(){
    freopen("1.txt","r",stdin);
    init();
    n=read();m=read();
    for(register int i=0;i<m;i++){
        u=read();v=read();w=read();
        insert(u,v,1);
    }
    int ans=0x3f3f3f3f;
    for(register int i=1;i<=n;i++){
        dijkstra(i);
        ans=min(ans,dis[i]);
    }
    cout<<ans<<endl;
    return 0;
}

相關文章