一、概述
周赛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位的。
三、总结
这个破题,一大堆解法,一个个看过去看得我没脾气。