一、概述
周賽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位的。
三、總結
這個破題,一大堆解法,一個個看過去看得我沒脾氣。