【點(diǎn)燈游戲】高斯消元法求解異或方程組

有沒(méi)有玩過(guò)“亮燈游戲”驼鞭?

有一個(gè)5*5的燈陣秦驯,初始狀態(tài)全是滅的,選擇一個(gè)燈進(jìn)行點(diǎn)亮挣棕,點(diǎn)亮的同時(shí)若其上下左右存在燈译隘,則需要將其一同點(diǎn)亮,如何操作可以點(diǎn)亮所有的燈洛心?

來(lái)拆解一下這個(gè)解謎:

  • 首先燈只有點(diǎn)亮和熄滅兩個(gè)狀態(tài)固耘,可以計(jì)作1和0,初始狀態(tài)是一個(gè)5*5的矩陣词身,值均為0厅目,目標(biāo)通過(guò)一系列操作,把所有值修改成1法严。

  • 其次损敷,一個(gè)燈的狀態(tài)被改變兩次,等于恢復(fù)初始值深啤,所以對(duì)于矩陣中的每個(gè)值嗤锉,要么操作一次,要么不操作墓塌。

  • 再看影響范圍瘟忱,位于中間部分的點(diǎn),一次操作影響5個(gè)值苫幢;位于邊上的點(diǎn)访诱,一次操作影響4個(gè)值;位于角上的點(diǎn)韩肝,一次操作影響3個(gè)值触菜。

image.png
  • 然后來(lái)看點(diǎn)燈操作的實(shí)質(zhì),若原來(lái)狀態(tài)是0哀峻,則操作過(guò)后變?yōu)?涡相,反之,原來(lái)狀態(tài)是1剩蟀,則操作之后變?yōu)?催蝗。這個(gè)操作本質(zhì)上可以看做和1進(jìn)行異或運(yùn)算,0^1=1; 1^1=0育特。

  • 最后來(lái)看最終解的形態(tài)丙号,值得注意的是,燈的狀態(tài)和操作的順序無(wú)關(guān),先點(diǎn)A再點(diǎn)B和先點(diǎn)B再點(diǎn)A達(dá)到的終態(tài)是一樣的(參考下圖)犬缨,所以最后的解應(yīng)該也是一個(gè)5*5矩陣喳魏,其中值為1代表這個(gè)燈需要點(diǎn),值為0代表這個(gè)燈不需要點(diǎn)怀薛。點(diǎn)燈順序不重要刺彩。

image.png

綜合以上幾點(diǎn),點(diǎn)燈問(wèn)題本質(zhì)上就被抽象為一個(gè)異或方程組枝恋,以3*3為例:


image.png

xn的取值0或1迂苛,代表該位置燈是否操作。

x1位置燈的狀態(tài)受x1,x2,x4是否被點(diǎn)的影響鼓择,最終目標(biāo)狀態(tài)為1三幻,所以可以寫(xiě)成:x1x2x4=1

同理對(duì)應(yīng)x2位置,可得x1x2x3x5=1呐能,對(duì)于x5位置念搬,x2x4x5x6^x8=1

這樣,有n個(gè)燈和n個(gè)位置摆出,就可以構(gòu)建異或方程組朗徊。

根據(jù)本科線(xiàn)性代數(shù)的知識(shí),方程組可以寫(xiě)成Ax=B的格式偎漫,通過(guò)高斯消元法來(lái)求解爷恳。求解方法是先通過(guò)線(xiàn)性變化,把矩陣轉(zhuǎn)換為上三角陣象踊,然后倒序確定所有變量的值温亲。具體過(guò)程去翻教材或者百度吧。

下面來(lái)看具體代碼實(shí)現(xiàn)杯矩。

首先定義一個(gè)Pos類(lèi)栈虚,表示謎題矩陣中的元素,包含值史隆、所在行魂务、列、統(tǒng)一編號(hào)index等泌射。

class Pos {
    private int x;
    private int y;
    private int row;
    private int col;
 
    int getX() {
        return x;
    }
 
    void setX(int x) {
        this.x = x;
    }
 
    int getY() {
        return y;
    }
 
    void setY(int y) {
        this.y = y;
    }
 
    int getRow() {
        return row;
    }
 
    void setRow(int row) {
        this.row = row;
    }
 
    int getCol() {
        return col;
    }
 
    void setCol(int col) {
        this.col = col;
    }
 
    Pos getPosByIndex(int index){
        this.setX((index-1)/this.col);
        this.setY((index-1)%this.col);
        return this;
    }
 
    int getPosIndex(){
        return this.x*this.col + this.y + 1;
    }
 
    boolean isInside() {
        return this.x >= 0 && this.x < this.row && this.y >= 0 && this.y < this.col;
    }
 
    Pos getPosByOffset(int dx, int dy){
        Pos pos = new Pos();
        pos.row = this.row;
        pos.col = this.col;
        pos.x = this.x + dx;
        pos.y = this.y + dy;
        return pos;
    }
}

然后來(lái)看主程序粘姜,大致包括這么幾個(gè)階段:

  1. 讀取初始謎題矩陣
  2. 將謎題矩陣轉(zhuǎn)換為異或方程組
  3. 求解方程組
  4. 給出解答矩陣
public class XorLinearEquation {
 
    private static final int[][] towards = {{0,1},{0,-1},{1,0},{-1,0}};
    private static int total;
    private static int[][] A;  //異或方程組
    private static int[][] puzzle;  //謎題矩陣
    private static int[][] answer;  //解答矩陣
    private static boolean haveLegalAnswer;
 
    private static void getEquations(){
        int rows = puzzle.length;
        int cols = puzzle[0].length;
        answer = new int[rows][cols];
        total = rows * cols;
        A = new int[total +2][total +2];
 
        Pos pos = new Pos();
        pos.setRow(rows);
        pos.setCol(cols);
 
        for(int i=0;i<rows;i++){
            for(int j=0;j<cols;j++){
                pos.setX(i);
                pos.setY(j);
                getEquationByPos(pos);
            }
        }
    }
 
    private static void getEquationByPos(Pos pos){
        int index = pos.getPosIndex();
        //自身位置必然受影響,total+1代表終態(tài)熔酷,根據(jù)初始值和1異或確定:若初始值為0孤紧,則影響總和需要為1;反之纯陨,影響總和需要為0坛芽。
        A[index][index] = 1;
        A[index][total+1] = puzzle[pos.getX()][pos.getY()]^1;
        for(int i=0;i<4;i++){
            //判斷上下左右四個(gè)方向是否有燈
            Pos offsetPos = pos.getPosByOffset(towards[i][0],towards[i][1]);
            if(!offsetPos.isInside()){
                continue;
            }
            //若有留储,則該位置的燈會(huì)受到index開(kāi)關(guān)影響翼抠,需要加進(jìn)方程組咙轩。
            int index2 = offsetPos.getPosIndex();
            A[index][index2] = 1;
        }
    }
 
    private static void swap(int line1, int line2){
        int temp;
        for(int i=1;i<=total+1;i++){
            temp = A[line1][i];
            A[line1][i] = A[line2][i];
            A[line2][i] = temp;
        }
    }
 
 
 
    private static void gauss(){
        // 高斯消元法
        int temp;
        for(int i=1;i<=total;i++){
            temp = i;
            for(int j=i+1;j<=total;j++){
                if(A[temp][i]==0 && A[j][i]==1){
                    temp = j;
                    break;
                }
            }
            if(A[temp][i]==0){
                continue;
            }
            if(temp!=i){
                swap(temp,i);
            }
            // 以上代碼作用:嘗試尋找第i列不為0的行,并將其交換至第i行阴颖,為消元構(gòu)成上三角矩陣做準(zhǔn)備活喊,若該列所有行均為0,則跳至下一列進(jìn)行判斷量愧。
            // 以下為消元過(guò)程钾菊,若有一行第i列為1,則與第i行進(jìn)行異或偎肃,確保其第i列位置為0煞烫,一輪循環(huán)后,當(dāng)前i階矩陣即變?yōu)樯先顷?            for(int j=i+1;j<=total;j++){
                if(A[j][i]==0){
                    continue;
                }
                for(int k=i;k<=total+1;k++){
                    A[j][k]^=A[i][k];
                }
            }
 
        }
 
        Pos pos = new Pos();
        pos.setRow(puzzle.length);
        pos.setCol(puzzle[0].length);
        answer = new int[pos.getRow()][pos.getCol()];
 
        for(int i=total;i>=1;i--){
            //若矩陣一行左側(cè)所有元素為0累颂,而右側(cè)不為0滞详,則方程無(wú)解
            if(!legalJudge(i)){
                haveLegalAnswer = false;
                break;
            }
            pos = pos.getPosByIndex(i);
            //從上三角矩陣右下角獲取解
            answer[pos.getX()][pos.getY()] = A[i][total+1];
            //遍歷檢查上方行數(shù)該列是否為0,若不為0紊馏,則將結(jié)果列異或進(jìn)行消元
            for(int j=i-1;j>=1;j--){
                if(A[j][i]==1){
                    A[j][total+1]^=A[i][total+1];
                }
            }
        }
    }
 
    private static boolean legalJudge(int index){
        for(int i = 1;i<=total;i++){
            if(A[index][i]!=0){
                return true;
            }
        }
        return A[index][total + 1] == 0;
    }
 
    private static void showAnswer(){
        for (int[] anAnswer : answer) {
            System.out.println(Arrays.toString(anAnswer));
        }
    }
 
    public static void main(String[] args){
        // initialize puzzle
        puzzle = new int[5][5];
        //====================================
        // Solve by Gauss elimination
        haveLegalAnswer = true;
        for (int[] aPuzzle : puzzle) {
            System.out.println(Arrays.toString(aPuzzle));
        }
        System.out.println("===============");
        getEquations();
        gauss();
        if(haveLegalAnswer) {
            showAnswer();
        } else {
            System.out.println("The puzzle has no solution! ");
        }
 
    }
}

對(duì)于開(kāi)頭的問(wèn)題料饥,初始矩陣就是5*5的值均為0的二維數(shù)組,求解結(jié)果如下:


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末朱监,一起剝皮案震驚了整個(gè)濱河市岸啡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赫编,老刑警劉巖巡蘸,帶你破解...
    沈念sama閱讀 216,919評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異擂送,居然都是意外死亡赡若,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)团甲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)逾冬,“玉大人,你說(shuō)我怎么就攤上這事躺苦∩砟澹” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,316評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵匹厘,是天一觀的道長(zhǎng)嘀趟。 經(jīng)常有香客問(wèn)我,道長(zhǎng)愈诚,這世上最難降的妖魔是什么她按? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,294評(píng)論 1 292
  • 正文 為了忘掉前任牛隅,我火速辦了婚禮,結(jié)果婚禮上酌泰,老公的妹妹穿的比我還像新娘媒佣。我一直安慰自己,他們只是感情好陵刹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布默伍。 她就那樣靜靜地躺著,像睡著了一般衰琐。 火紅的嫁衣襯著肌膚如雪也糊。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,245評(píng)論 1 299
  • 那天羡宙,我揣著相機(jī)與錄音狸剃,去河邊找鬼。 笑死狗热,一個(gè)胖子當(dāng)著我的面吹牛钞馁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斗搞,決...
    沈念sama閱讀 40,120評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼指攒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了僻焚?” 一聲冷哼從身側(cè)響起允悦,我...
    開(kāi)封第一講書(shū)人閱讀 38,964評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虑啤,沒(méi)想到半個(gè)月后隙弛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狞山,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評(píng)論 2 333
  • 正文 我和宋清朗相戀三年全闷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萍启。...
    茶點(diǎn)故事閱讀 39,764評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡总珠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出勘纯,到底是詐尸還是另有隱情局服,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評(píng)論 5 344
  • 正文 年R本政府宣布驳遵,位于F島的核電站淫奔,受9級(jí)特大地震影響删顶,放射性物質(zhì)發(fā)生泄漏惕蹄。R本人自食惡果不足惜养涮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評(píng)論 3 327
  • 文/蒙蒙 一后频、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧唐责,春花似錦鳞溉、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,697評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)月弛。三九已至肴盏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間帽衙,已是汗流浹背菜皂。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,846評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厉萝,地道東北人恍飘。 一個(gè)月前我還...
    沈念sama閱讀 47,819評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像谴垫,于是被迫代替她去往敵國(guó)和親章母。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容