AcWing 175 電路維修

題目描述:

達達是來自異世界的魔女,她在漫無目的地四處漂流的時候,遇到了善良的少女翰翰,從而被收留在地球上。

翰翰的家裏有一輛飛行車。

有一天飛行車的電路板突然出現了故障,導致無法啓動。

電路板的整體結構是一個R行C列的網格(R,C≤500),如下圖所示。

電路.png

每個格點都是電線的接點,每個格子都包含一個電子元件。

電子元件的主要部分是一個可旋轉的、連接一條對角線上的兩個接點的短電纜。

在旋轉之後,它就可以連接另一條對角線的兩個接點。

電路板左上角的接點接入直流電源,右下角的接點接入飛行車的發動裝置。

達達發現因爲某些元件的方向不小心發生了改變,電路板可能處於斷路的狀態。

她準備通過計算,旋轉最少數量的元件,使電源與發動裝置通過若干條短纜相連。

不過,電路的規模實在是太大了,達達並不擅長編程,希望你能夠幫她解決這個問題。

注意:只能走斜向的線段,水平和豎直線段不能走。

輸入格式

輸入文件包含多組測試數據。

第一行包含一個整數T,表示測試數據的數目。

對於每組測試數據,第一行包含正整數R和C,表示電路板的行數和列數。

之後R行,每行C個字符,字符是"/""\"中的一個,表示標準件的方向。

輸出格式

對於每組測試數據,在單獨的一行輸出一個正整數,表示所需的縮小旋轉次數。

如果無論怎樣都不能使得電源和發動機之間連通,輸出NO SOLUTION。

數據範圍

1≤R,C≤500,
1≤T≤5

輸入樣例:

1
3 5
\\/\\
\\///
/\\\\

輸出樣例:

1

樣例解釋

樣例的輸入對應於題目描述中的情況。

只需要按照下面的方式旋轉標準件,就可以使得電源和發動機之間連通。

電路2.png

分析:

我們知道BFS常用於求解邊權相同的最短路問題,但是部分邊權不等的最短路問題也可以用BFS求,本題就是其中的一個例子。

當一個圖的邊權只可能是0或者1時,就可以使用雙端隊列BFS求解。本題求從左上角到右下角要想連通的最小旋轉次數,一個方格的對角線之間,如果有連線,就可以被視爲這兩點之間的邊權長度是0,如果需要旋轉纔有連線,則視邊權的長度爲1,所以本題就轉化爲了一個邊權只有01兩種情況的最短路問題。爲什麼BFS可以解決邊權不等的最短路問題,可以說是此時BFS的隊列相當於dijkstra算法中的優先級隊列,是單調的,也是由BFS隊列中元素的兩段性和單調性決定的。

首先回憶下dijkstra算法,AcWing 849 Dijkstra求最短路 IAcWing 850 Dijkstra求最短路 II中分別介紹了dijkstra算法的樸素版解法和堆優化版解法。簡單的說,d[i]表示第i個點到起點的距離,dijkstra算法的思路就是先將起點加入點集,然後在點集中取出一個d最小的點,去更新相鄰點的d數組,如果某個點被鬆弛了,就加入優先級隊列,也就等價於加入了點集,已經出隊的點不再參與更新操作,每次取還未出隊的離起點距離最小的點取進行鬆弛操作。

再說下BFS的兩段性和單調性,兩段性可以理解爲BFS過程中用到的隊列中的頂點最多涉及BFS生成樹中兩層頂點,因爲BFS相當於對樹做層次遍歷,這個性質是顯然的,兩段性意味着任何時刻隊列中的點離起點的距離差最多是1。單調性指的是從隊頭到隊尾的頂點離起點的距離是單調增加的。兩段性和單調性保證了BFS算法求出的一定是最短路徑。本題要考慮的是如何在邊權不等的情況下維持隊列的這兩個性質,邊權有01之分,遍歷到邊權爲0和1的邊我們要如何如何處理才能夠維持隊列的單調性。取出隊頭元素後,如果當前邊權是0,就加入隊頭;當前邊權是1,就加入隊尾,很顯然這種操作維持了隊列的兩段性和單調性,只是需要雙端隊列實現而已。

既然知道了這樣操作在BFS過程中隊列始終是單調的,並且隊頭元素一定是距離最近的,下面要做的就是仿照dijkstra算法求最短路了,這裏的雙端隊列就相當於dijkstra中的優先級隊列。流程如下:

首先,將起點加入隊列,接着當隊列非空時執行如下循環:

取出隊頭元素,如果隊頭元素還沒有出隊過,就去更新周圍點的最短距離,如果周圍的點被鬆弛了,就將該點也加入隊列,如果松弛的邊權是0,就加入隊頭,否則加入隊尾。

當然,本題的原理部分只有上面這麼多,但是實現的細節值得注意的地方卻很多。首先,由於只能斜着走,所以橫縱座標要麼同時加1,要麼同時減1,要麼一個加1一個減1。不論是那種,從起點(0,0)出發,能夠到達的點的橫縱座標之和一定是偶數,所以當R + C是奇數時,就無解。第二個要注意的是,題目給定的是格子內部的線的方向,一共有R * C個格子,但是一個有(R+1)*(C + 1)個點,我們讀取線只需要從(0,0)讀到(r-1,c-1),求的是(0,0)到(r,c)的最短距離,這點一定要注意。第三點,本題將方向向量玩出了新花樣,從左邊(x,y)出發,能夠到達哪些點呢?可以到達的點的方向向量是dx[] = {-1,-1,1,1};dy[] = {-1,1,1,-1},毫無疑問。要想到達這四點需要經過哪些座標的格子呢?不難推出需要經過格子的方向向量依次是ix[] = {-1,-1,0,0};iy[] = {-1,0,0,-1};然後這四個格子中的線是朝什麼方向的纔可以不用旋轉,char op[] = "\\/\\/";,就是op中定義的這四個方向,注意\前面要加轉義字符\。最後一點,也是相當重要的一點,標誌數組st應該在何處剪枝,是否需要剪枝?三個地方可以放置剪枝的語句,第一個地方,取出隊頭元素時,如果隊頭元素已經出過隊,就不用再去執行鬆弛操作了,這也是推薦加入剪枝語句的地方;第二個地方,在判斷某個點距離是否可以被更新前,如果一個點已經出過隊,說明最短距離已經確定,鬆弛操作必將是徒勞的,所以可以剪枝,但是效果不大,因爲並沒有節省多少操作;第三個地方,鬆弛操作後,將被鬆弛的點加入隊列前,如果這個點出過隊,就不再入隊,這個地方剪枝毫無意義,因爲已經出過隊的元素最短路徑長度已經確定了,不可能再被鬆弛。既然出過隊的元素不會再背鬆弛,自然不會再次被加入隊列,這是否意味着每個頂點只會入隊一次呢?如果是,剪枝操作將毫無意義,我開始也是這樣以爲的,加不加剪枝語句提交時間都差不多,但這僅僅是數據水的緣故,爲此,還特地問了下y總。之後纔想起來,儘管每個頂點出隊後不會再次被鬆弛,不會再入隊,但是出隊前卻可能多次入隊,比如C點被A點鬆弛先入隊了,之後又被B點鬆弛,再次入隊,然後C第一次出隊,執行鬆弛操作,當C第二次出隊時,因爲標誌數組是true,便會跳過C,此時的鬆弛操作毫無意義,而且耗費時間,這也是爲何要在出隊時剪枝的原因。這裏的剪枝操作dijkstra也同樣適用,或者說就是由dijkstra的剪枝操作推廣來的。

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 505;
typedef pair<int,int> PII;
char g[N][N];
int r,c,d[N][N];
deque<PII> q;
int dx[] = {-1,-1,1,1};
int dy[] = {-1,1,1,-1};
int ix[] = {-1,-1,0,0};
int iy[] = {-1,0,0,-1};
char op[] = "\\/\\/";
bool st[N][N];
void bfs(int x,int y){
    memset(d,0x3f,sizeof d);
    memset(st,false,sizeof st);
    q.push_front({x,y});
    d[x][y] = 0;
    while(q.size()){
        PII t = q.front();
        q.pop_front();
        if(st[t.first][t.second])   continue;
        st[t.first][t.second] = true;
        for(int i = 0;i < 4;i++){
            int nx = t.first + dx[i],ny = t.second + dy[i];
            if(nx < 0 || nx > r || ny < 0 || ny > c)  continue;
            int ex = t.first + ix[i],ey = t.second + iy[i];
            int dist = d[t.first][t.second] + (g[ex][ey] != op[i]);
            if(dist < d[nx][ny]){
                d[nx][ny] = dist;
                if(g[ex][ey] == op[i])  q.push_front({nx,ny});
                else    q.push_back({nx,ny});
            }
        }
    }
}
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&r,&c);
        for(int i = 0;i < r;i++)    scanf("%s",g[i]);
        if((r + c) & 1) puts("NO SOLUTION");
        else{
            bfs(0,0);
            printf("%d\n",d[r][c]);
        }
    }
    return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章