1. 程式人生 > 其它 >Java註解和反射05:獲取泛型和註解

Java註解和反射05:獲取泛型和註解

題目傳送門

一、傳遞閉包

本題考察\(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表示ij之間的偏序關係還沒有確定下來,就需要繼續讀取下一對二元關係;如果所有的關係都確定了,就返回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了,表示之前的演算法已經使用了ab這條邊了,就不需要再執行傳遞閉包演算法了,如果f[a][b] = 0,也只需要更新與ab有關點的關係。

如上圖所示,加入ab這條邊後,我們只需要遍歷能夠到達a的所有點x以及b能夠到達所有的點y,用平方級複雜度就可以完成加一條邊後關係的更新。f[a][b] = 1,首先我們需要更新f[x][b]f[a][y]的值為1,表示x可以到達b了,a可以到達y了,最後再更新f[x][y] = 1。注意這裡的xy都是泛指,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\)思想呢?似乎就是完全鬆弛,不怕費時間吧。