Python日誌模組封裝
一、傳遞閉包
本題考察\(Floyd\)演算法在傳遞閉包問題上的應用。給定若干對元素和若干對二元關係,並且關係具有傳遞性,通過傳遞性推匯出儘量多的元素之間的關係的問題被稱為傳遞閉包。比如\(a < b,b < c\),就可以推匯出\(a < c\),如果用圖形表示出這種大小關係,就是\(a\)到\(b\)有一條有向邊,\(b\)到\(c\)有一條有向邊,可以推出\(a\)可以到達\(c\),找出圖中各點能夠到達點的集合,就類似於\(Floyd\)演算法求圖中任意兩點間的最短距離。\(Floyd\)求解傳遞閉包問題的程式碼如下:
void floyd(){ for(int k = 0;k < n;k++) for(int i = 0;i < n;i++) for(int j = 0;j < n;j++) f[i][j] |= f[i][k] & f[k][j]; }
只是對原來演算法在狀態轉移方程上略加修改 就能夠求解傳遞閉包問題了。【套路,滿滿的套路,你不學習就不會的套路】f[i][j] = 1
表示i
可以到達j
(i < j
),f[i][j] = 0
表示i
不可到達j
。只要i
能夠到達k
並且k
能夠到達j
,那麼i
就能夠到達j
,這就是上面程式碼的含義。
對於本題而言,給定\(n\)個元素和一堆二元關係,依次讀取每個二元關係,在讀取第\(i\)個二元關係後,如果可以確定\(n\)個元素兩兩間的大小關係了,就輸出在幾對二元關係後可以確定次序,並且次序是什麼;如果出現了矛盾,就是\(A < B\)並且\(B < A\)這種情況發生了就輸出多少對二元關係後開始出現矛盾;如果遍歷完所有的二元關係還不能確定所有元素間的大小關係,就輸出無法確定。
可以發現,題目描述要求按順序遍歷二元關係,一旦前\(i\)個二元關係可以確定次序了就不再遍歷了,即使第\(i + 1\)對二元關係就會出現矛盾也不去管它了。對於二元關係的處理和之前的做法一樣,\(A < B\),就將\(f[0][1]\)設為\(1\),題目字母只會在\(A\)到\(Z\)間,因此可以對映為\(0\)到\(25\)這\(26\)個元素,如果\(f[0][1] = f[1][0] = 1\),就可以推出f[0][0] = 1
,此時\(A < B\)並且\(A > B\)發生矛盾,因此在f[i][i]= 1
時發生矛盾。
下面詳細分析下求解的步驟:首先每讀取一對二元關係,就執行一遍\(Floyd\)
check
函式判斷下此時是否可以終止遍歷,如果發生矛盾或者次序全部被確定就終止遍歷,否則繼續遍歷。在確定所有的次序後,需要輸出偏序關係,因此需要執行下getorder
函式。注意這裡的終止遍歷僅僅是不再針對新增的二元關係去求傳遞閉包,迴圈還是要繼續的,需要讀完資料才能繼續讀下一組資料。
下面設計\(check\)函式和\(getorder\)函式。
int check(){
for(int i = 0;i < n;i++)
if(f[i][i]) return 0;
for(int i = 0;i < n;i++){
for(int j = 0;j < i;j++){
if(!f[i][j] && !f[j][i]) return 1;
}
}
return 2;
}
如果f[i][i] = 1
就發生矛盾了,可以返回了;如果f[i][j] = f[j][i] = 0
表示i
與j
之間的偏序關係還沒有確定下來,就需要繼續讀取下一對二元關係;如果所有的關係都確定了,就返回2。
string getorder(){
char s[26];
for(int i = 0;i < n;i++){
int cnt = 0;
for(int j = 0;j < n;j++) cnt += f[i][j];
s[n - cnt - 1] = i + 'A';
}
return string(s,s + n);
}
確定所有元素次序後如何判斷元素i
在第幾個位置呢?f[i][j] = 1
表示i < j
,因此計算下i
小於元素的個數cnt
,就可以判定i
是第cnt + 1
大的元素了。
總的程式碼如下:
#include <bits/stdc++.h>
// Floyd解決傳送閉包問題
using namespace std;
const int N = 27;
int n; // n個變數
int m; // m個不等式
int g[N][N]; //原始關係
int f[N][N]; //推導的關係
void floyd() {
//複製出來f
memcpy(f, g, sizeof g);
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
// i可以到達k,k可以到達j,那麼i可以到達j
//為了防止列舉其它k'時,導致被覆蓋成0,所以寫成“或”的形式
f[i][j] |= f[i][k] & f[k][j];
}
// 1:可以確定兩兩之間的關係,2:矛盾,3:不能確定兩兩之間的關係
int check() {
//如果i<i,那麼就是出現了矛盾
for (int i = 0; i < n; i++)
if (f[i][i]) return 2;
//存在還沒有識別出關系的兩個點i,j,還要繼續讀入
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!f[i][j] && !f[j][i]) return 3;
return 1;
}
//升序輸出所有變數
string getorder() {
char s[26];
for (int i = 0; i < n; i++) {
//這個思路很牛X!
int cnt = 0;
// f[i][j] = 1表示i可以到達j (i< j)
for (int j = 0; j < n; j++) cnt += f[i][j]; //比i大的有多少個
//舉個栗子:i=0,表示字元A
//比如比i大的有5個,共6個字元:ABCDEF
// n - cnt - 1 = 6-5-1 = 0,也就是A放在第一個輸出的位置上
//之所以再-1,是因為下標從0開始
s[n - cnt - 1] = i + 'A';
}
//轉s字元陣列為字串
return string(s, s + n);
}
int main() {
// n個變數,m個不等式
// 當輸入一行 0 0 時,表示輸入終止
while (cin >> n >> m, n || m) {
string str;
int type = 3; // 3:不能確定兩兩之間的關係
//初始化原始關係,準備讀入資料
memset(g, 0, sizeof g);
// m條邊,下面需要輸出在第幾個輸入後有問題,所以需要用for迴圈
for (int i = 1; i <= m; i++) {
cin >> str;
//如果不是待確定,就表示是已確定或者出現了矛盾,就沒有必要再處理了
//但是,還需要耐心的讀取完畢,因為可能還有下一輪,不讀入完耽誤下一輪
if (type != 3) continue;
//變數只可能為大寫字母A~Z,對映到0~25
int a = str[0] - 'A', b = str[2] - 'A';
g[a][b] = 1; //記錄a<b
//跑一遍最短路,傳遞閉包
floyd();
//檢查一下現在的情況,是不是已經可以判定了
type = check();
//出現的矛盾
if (type == 2)
printf("Inconsistency found after %d relations.\n", i);
else if (type == 1) { //可以確定了
//輸出升序排列的所有變數
string ans = getorder();
printf("Sorted sequence determined after %d relations: %s.\n", i, ans.c_str());
}
}
//所有表示式都輸入了,仍然定不下來關係
if (type == 3) printf("Sorted sequence cannot be determined.\n");
}
return 0;
}
二、優化演算法
分析上面的程式碼可以發現,每讀取一對二元關係就去執行一次\(floyd\)演算法,時間複雜度是\(O(m*n^3)\),顯然冗餘度很高,新增了\(a\)與\(b\)的大小關係,只需要更改由這條邊可傳遞下去的關係即可,比如之前執行\(floyd\)已經確定\(A < C\),新增了\(B < D\),完全沒必要再去求解\(A\)與\(C\)的大小關係了。因此,如果新讀入的二元關係f[a][b]
已經是1了,表示之前的演算法已經使用了a
與b
這條邊了,就不需要再執行傳遞閉包演算法了,如果f[a][b] = 0
,也只需要更新與a
、b
有關點的關係。
如上圖所示,加入a
與b
這條邊後,我們只需要遍歷能夠到達a
的所有點x
以及b
能夠到達所有的點y
,用平方級複雜度就可以完成加一條邊後關係的更新。f[a][b] = 1
,首先我們需要更新f[x][b]
與f[a][y]
的值為1
,表示x
可以到達b
了,a
可以到達y
了,最後再更新f[x][y] = 1
。注意這裡的x
與y
都是泛指,x
指的是能夠到達a
的點,不一定是圖中標的與a
直接相連的那個點,也可能是圖中x
的上一點,也是可以到達a
的。這樣一來每讀入一條邊最多隻要平方級別複雜度就可以完成更新,總的時間複雜度為\(O(m*n^2)\),效率也大幅提升了。這種方法的程式碼如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 27;
int n, m, f[N][N];
// 1:可以確定兩兩之間的關係,2:矛盾,3:不能確定兩兩之間的關係
int check() {
//如果i<i,那麼就是出現了矛盾
for (int i = 0; i < n; i++)
if (f[i][i]) return 2;
//存在還沒有識別出關系的兩個點i,j,還要繼續讀入
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!f[i][j] && !f[j][i]) return 3;
return 1;
}
//升序輸出所有變數
string getorder() {
char s[26];
for (int i = 0; i < n; i++) {
//這個思路很牛X!
int cnt = 0;
// f[i][j] = 1表示i可以到達j (i< j)
for (int j = 0; j < n; j++) cnt += f[i][j]; //比i大的有多少個
//舉個栗子:i=0,表示字元A
//比如比i大的有5個,共6個字元:ABCDEF
// n - cnt - 1 = 6-5-1 = 0,也就是A放在第一個輸出的位置上
//之所以再-1,是因為下標從0開始
s[n - cnt - 1] = i + 'A';
}
//轉s字元陣列為字串
return string(s, s + n);
}
int main() {
// n個變數,m個不等式
// 當輸入一行 0 0 時,表示輸入終止
while (cin >> n >> m, n || m) {
string str;
int type = 3;
memset(f, 0, sizeof f);
for (int i = 1; i <= m; i++) {
cin >> str;
if (type != 3) continue;
int a = str[0] - 'A', b = str[2] - 'A';
if (!f[a][b]) { //如果a和b還沒有確定關係
f[a][b] = 1; //記錄a<b
for (int x = 0; x < n; x++) { //列舉所有節點
if (f[x][a]) f[x][b] = 1; //如果x<a,那麼x一定小於b
if (f[b][x]) f[a][x] = 1; //如果b<x,那麼a一定小於x
//外層迴圈找出所有小於a的點x,內層迴圈找出所有大於b的點y
//記錄x,y的關係為x<y
for (int y = 0; y < n; y++)
if (f[x][a] && f[b][y]) f[x][y] = 1;
}
}
//檢查一下現在的情況,是不是已經可以判定了
type = check();
//出現的矛盾
if (type == 2)
printf("Inconsistency found after %d relations.\n", i);
else if (type == 1) { //可以確定了
//輸出升序排列的所有變數
string ans = getorder();
printf("Sorted sequence determined after %d relations: %s.\n", i, ans.c_str());
}
}
//所有表示式都輸入了,仍然定不下來關係
if (type == 3) printf("Sorted sequence cannot be determined.\n");
}
return 0;
}
其實這種優化演算法就和#Floyd#沒有關係了。也可以認為是一種基於\(Floyd\)思想的優化版本,什麼是\(Floyd\)思想呢?似乎就是完全鬆弛,不怕費時間吧。