【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位的。

三、总结

这个破题,一大堆解法,一个个看过去看得我没脾气。

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