【LeetCode】Weekly Contest 160:Circular Permutation in Binary Representation & 89. Gray Code 格雷碼輸出

一、概述

周賽160中第二題二進制排列和89題格雷碼輸出類似。只不過後者默認起點爲1,而前者可以自己指定起點。因此在這裏我以前者爲例進行分析。

輸入兩個數字,第一個爲格雷碼位數,第二個爲格雷碼十進制起始值。返回一個格雷碼數組,元素均爲十進制數字。

我做的好蠢,二進制處理一直是我的弱點。因爲這類題目看的很少。

這是周賽第二題的結果:

這是89題的結果。

很奇怪,我都是用的一個方法,後者的成績比前者好得多。

二、分析

1、我的方法

格雷碼是什麼?前一個數字和後一個數字的二進制表示形式只改變一位。這就是格雷碼。比如說兩位格雷碼,第一個是00,第二個可以是01也可以是10,第三個對應着都是11,第四個對應着是10或01。

最開始我想找一些花活——比如說十進制數字,偶數+1表示二進制第0位由0變1;+4等價於二進制第三位由0變1。但是問題來了,通過十進制爲偶數我可以知道第0位爲0,怎麼知道第三位爲0呢?沒找到規律。就很煩。

那怎麼辦呢?最暴力的解法:維護一個set,將起始數字化爲二進制放入set;從第一位開始變化一位得到一個新的二進制數,判斷set中是否存在該數,若存在,不變化這一位,變化下一位;若不存在,將該數放入set,其十進制放入向量。然後對新的二進制數再從第一位開始變換。當set中元素個數達到2^n,n爲二進制位數時,輸出向量即可。

沒有一點的算法思想,就是單純的處理輸出。爲了從二進制變回十進制,我還特地寫了一個函數。真是哭笑不得。這方法太蠢了。

代碼如下:

class Solution {
    unordered_set<string> ss;
public:
    vector<int> circularPermutation(int n, int start) {
        vector<int> res;
        res.push_back(start);
        string tmp="";
        if(start==0)
        {
            while(tmp.size()<n)
                tmp+='0';
        }
        else
        {
            while(start!=1)
            {
                tmp+='0'+start%2;
                start=start/2;
            }
            tmp+='1';
            reverse(tmp.begin(),tmp.end());
            while(tmp.size()<n)
                tmp='0'+tmp;
        }
        cout<<tmp<<'\n';
        ss.insert(tmp);
        while(res.size()<pow(2,n))
        {
            for(int i=0;i<n;i++)
            {
                tmp[i]=(tmp[i]-'0'+1)%2+'0';
                //cout<<tmp<<'\n';
                //cout<<s4i(tmp)<<'\n';
                if(ss.find(tmp)==ss.end())
                   {
                       res.push_back(s4i(tmp));
                       ss.insert(tmp);
                    break;
                   }
                   else
                   {
                       tmp[i]=(tmp[i]-'0'+1)%2+'0';
                   }
            }
        }
        return res;
    }
    int s4i(string s)
    {
        int res=0;
        for(int i=0;i<s.size();i++)
        {
            res+=pow(2,s.size()-i-1)*(s[i]-'0');
        }
        return res;
    }
};

2、較好的方法,位操作

vector<int> res;
for (int i = 0; i < 1 << n; ++i)
    res.push_back(start ^ i ^ i >> 1);
return res;

wdnmd,人家兩行寫完我幾十行的功能,還比我跑的快跑得好。我找誰說理去啊。

參見該網址。所以說我的思路有問題:我的思路是對於一個已有的二進制數,從左往右修改一位,若其之前沒出現過,就選擇它。實際上呢?實際上若對應每一個十進制數,都確定一個二進制數與其對應,則事情就變得簡單了。這也是第89題的想法。在上面的網址中,我們可以得到一個由上一位格雷碼得到下一個格雷碼的公式:

對於一個四位格雷碼b3b2b1b0,得到下一個格雷碼c3c2c1c0,需要進行如下計算:

c3=b3

c2=b2異或b3

c1=b1異或b2

c0=b0異或b1

也就可以轉爲如下的形式:c3c2c1c0=b3b2b1b0異或0b3b2b1。這也就是

 i ^ i >> 1

的含義。

同時,我們也知道,異或的含義,可以看做是判斷兩個數是否相同,相同就是0,不同就是1。那麼,我現在已經有g0、g1、g2 ... gn一共n+1個格雷碼,對應0~n的數字。很明顯,g0=00...00。我現在有一個問題,我想得到以start爲開頭的一套“相鄰數字二進制差一位的序列”。怎麼辦?

答案爲用start和g0~gn做異或。爲什麼?

已知gk和gk+1之間差一位,start與gk做異或,得到的結果是它們“位之間的差異”,也就是相同爲0,不同爲1;start和gk+1做異或,同樣得到它們之間的差異。那麼由於gk和gk+1之間差一位,則start和它們做異或的結果也必定差一位。如此就得到了一個新的“相鄰數字二進制差一位的序列”。由於g0一直是0,因此start和g0做異或一定結果爲start,也就滿足了這個條件。

十分巧妙。

3、較好的方法,DFS回溯

class Solution {
    void utils(bitset<32>& bits, vector<int>& result, int k){
        if (k==0) {
            result.push_back(bits.to_ulong());
        }
        else {
            utils(bits, result, k-1);
            bits.flip(k-1);
            utils(bits, result, k-1);
        }
    }
public:
    vector<int> grayCode(int n) {
        bitset<32> bits;
        vector<int> result;
        utils(bits, result, n);
        return result;
    }
};

回溯法簡而言之就是將解空間寫成樹狀,然後使用DFS尋找所有解。但是這道題我畫不出樹,只能口述一下算法:

假設是四位,從左往右是1234位。那麼我從第一個,假設是0000,如何得到下一個?我把最後一位取反,就是0001。現在第四位取反完了,如何得到下一個?把第三位取反,也就是0011。再下一個?又把第四位取反,也就是0010。再下一個?第四位和第三位都取反完了,禍害第二位去,把第二位取反,0110,然後禍害第四位,0111,然後第三位,0101,然後第四位,0100。第二位之前禍害完了,禍害第一位。取反的位次依次是4,3,4,2,4,3,4,1,4,3,4,2,4,3,4。

如果實在要畫,我也只能畫出一條長長的樹,不好看。只需要知道它的思想:在改變第二位之後,是改變第四位,然後三位。每次我更改一個較小的位,下一次一定更改最後一位,然後往回。利用每次更改一位而寫出的遞歸,真想分析過程還是有點蛋疼的。

4、較好的方法,DP

class Solution {
public:
    vector<int> grayCode(int n) {
        if ( n == 0 ) {
            vector<int> v;
            v.push_back(0);
            return v;
        }
        vector<int> F[n+1];
        vector<int> v;
        v.push_back(0);
        v.push_back(1);
        F[1] = v;
        for ( int i = 2; i < n+1; i++ ) {
            for ( int j = 0; j < F[i-1].size(); j++ )
                F[i].push_back(F[i-1][j]);
            for ( int j = F[i-1].size()-1; j >= 0 ; j-- ) 
                F[i].push_back(F[i-1][j] + 1 << (i-1) );
        }
        return F[n];
    }
};

簡而言之,利用了這樣一條規律:

先生成一位的,在此基礎上生成二位的,最後生成n位的。

三、總結

這個破題,一大堆解法,一個個看過去看得我沒脾氣。

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