剑指offer学习笔记:6.4 抽象建模能力

建模的第一步是选择合理的数据结构来表达问题。实际生活中的问题千变万化,但是数据结构就只有固定的几种。选择合理的数据结构来表达问题,也就是建立模型。
建模的第二步是分析模型的内在规律,并用变成语言表达其规律。

面试题43:n个骰(tou二声)子的点数

把n个骰子扔在地上,所有的骰子朝上一面的点数和为s。输入n,打印出s的所有的可能值出现的概率
leetcode链接 https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/

class Solution {
public:
   vector twoSum(int n) {
       vector result;
       if (n < 1)
       {
           return result;
       }
       // 为了好交替调用,将两个矩阵定义为二维矩阵
       int* sum[2];
       sum[0] = new int[n*6 + 1];  // 所有可能和的总数,每位存放的就是该位对应和出现次数
       sum[1] = new int[n*6 + 1];
       int flag = 0;
       for(int i = 0; i < n*6 + 1; i++)
       {
           sum[0][i] = 0;
           sum[1][i] = 0;
       }
       for(int i = 1; i <= 6; i++)
       {
           sum[0][i] = 1;    // 只有一个骰子的情况
       }
       for(int k = 2; k <= n; k++)
       {
           for(int i = 0; i < k; i++) // k个骰子最小和是k,因此小于k是不可能了
           {
               sum[1-flag][i] = 0;
           }
           for(int i = k ; i < n*6 + 1; i++)  // 当点数大于k,考虑当前骰子点数和=上一次骰子点数和-1....前一次骰子点数和-6
           {
               sum[1-flag][i] = 0;
               for(int j = 1; j <= 6 && j <= i; j++)
               {
                   sum[1-flag][i] += sum[flag][i-j];
               }
           }
           flag = 1 -flag;
       }
       double total = pow(6, n);
       for(int i = n; i <= 6*n; i++)
       {
           result.push_back(sum[flag][i] / total);
       }
       return result;
   }
};

解题思路:
思路1:全排列。建立数组,每次排列后sum=n,则数组第n位+1,最后循环数组,计算每一个非零位概率。全排列用递归,会超时。这里递归思路用的是每次固定数组中一位,更正规一点的思想是用回溯。定义一个栈,每次循环push进当前位固定好的元素,递归调用n-1函数,函数退出后将push进元素pop出来。

class Solution {
public:
    int all = 0;
    void fullPer(int* arr, map& sum, int index, int n)
    {
        if (index >= n - 1)
        {
            all++;
            int tmp = 0;
            for(int i = 0; i < n; i++)
            {
                // cout << arr[i] << endl;
                tmp = tmp + arr[i];
            }
            sum[tmp]++;
            return;
        }
        for(int i = 1; i < 7; i++)
        {
            arr[index+1] = i;
            fullPer(arr, sum, index+1, n);
        }
        return;
    }
    vector twoSum(int n) {
        vector result;
        map sum;
        int *arr = new int[n];
        for(int i = 1; i < 7; i++)
        {
            arr[0] = i;
            fullPer(arr, sum, 0, n);
        }
        map::iterator iter = sum.begin();
        while(iter != sum.end())
        {
            result.push_back(iter->second / double(all));
            iter++;
        }
        return result;
    }
};

思路2:用两个数组来存储骰子点数的每一个总数出现次数。在一次循环中,第一个数组中的第n个数字表示骰子和为n出现的次数。在下一次循环中,我们加上一个新的骰子,此时和为n的骰子出现的次数应该等于上一次循环中骰子点数和为n-1,n-2...n-6的和,所以我们把另一个数组的n位设置为n-1,n-2...n-6的和。最后被循环的数组就代表全排列后的结果。

class Solution {
public:
    vector twoSum(int n) {
        vector result;
        if (n < 1)
        {
            return result;
        }
        // 为了好交替调用,将两个矩阵定义为二维矩阵
        int* sum[2];
        sum[0] = new int[n*6 + 1];  // 所有可能和的总数,每位存放的就是该位对应和出现次数
        sum[1] = new int[n*6 + 1];
        int flag = 0;
        for(int i = 0; i < n*6 + 1; i++)
        {
            sum[0][i] = 0;
            sum[1][i] = 0;
        }
        for(int i = 1; i <= 6; i++)
        {
            sum[0][i] = 1;    // 只有一个骰子的情况
        }
        for(int k = 2; k <= n; k++)
        {
            for(int i = 0; i < k; i++) // k个骰子最小和是k,因此小于k是不可能了
            {
                sum[1-flag][i] = 0;
            }
            for(int i = k ; i < n*6 + 1; i++)  // 当点数大于k,考虑当前骰子点数和=上一次骰子点数和-1....前一次骰子点数和-6
            {
                sum[1-flag][i] = 0;
                for(int j = 1; j <= 6 && j <= i; j++)
                {
                    sum[1-flag][i] += sum[flag][i-j];
                }
            }
            flag = 1 -flag;
        }
        double total = pow(6, n);
        for(int i = n; i <= 6*n; i++)
        {
            result.push_back(sum[flag][i] / total);
        }
        return result;
    }
};

面试题44:扑克牌的顺子

从扑克牌中随机抽5张牌,判断是不是顺子。扑克牌中2-10为数字本身,A为1,J为11,Q为12,K为13。而大王小王可以看做任意数字。
leetcode链接 https://leetcode-cn.com/problems/bu-ke-pai-zhong-de-shun-zi-lcof/

class Solution {
public:
   bool isStraight(vector& nums) {
       if (nums.size() == 0)
       {
           return false;
       }
       int max = nums[0];
       int min = nums[0];
       int zeroNum = 0;
       vector tmpVec;
       for(int i = 0; i < nums.size(); i++)
       {
           int tmp;
           if (nums[i] == 0)
           {
               zeroNum++;
               continue;
           }
           if (nums[i] == 'A')
           {
               tmp = 1;
           } else if (nums[i] == 'J')
           {
               tmp = 11;
           } else if (nums[i] == 'Q')
           {
               tmp = 12;
           } else if (nums[i] == 'K')
           {
               tmp = 13;
           }
           else {
               tmp = nums[i];
           }
           if (find(tmpVec.begin(), tmpVec.end(), tmp) == tmpVec.end())
           {
               tmpVec.push_back(tmp);
           }
           else{
               return false;
           }
           if (tmp < min || min == 0)
           {
               // cout << "min: " << min << endl;
               min = tmp;
           }
           if (tmp > max || max == 0)
           {
               max = tmp;
           }
       }
       // cout << "max: " << max << " min: " << min << " zeroNum: " << zeroNum << " size: " << nums.size() << endl;
       return (max - min + 1) <= nums.size();
   }
};

解题思路:由于大小王可以是任意数,将其设为0,方便操作 1)数组排序 2)记录数组中0的个数,记录数组最大值-非零最小值的差 + 1=数组长度+n,若n=0的个数,则为顺子,否则,不是。
转变下思路,感觉不排序也可以,遍历,最大,最小值,无相等元素,若最大值-最小值+1 <= 数组长度,则为顺子。

面试题45:圆圈中最后剩下的数字(约瑟夫环)

0,1....n-1这n个数字排成一个圆圈,从数字0开始,每次从圆圈中删除第m个数字,求圆圈中剩余的最后一个数字。
leetcode链接 https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/

class Solution {
public:
   int lastRemaining(int n, int m) {
       if (n == 0)
       {
           return 0;
       }
       if (m == 1) // 总是删除当前节点,最后获得的一定是最后一个节点
       {
           return n - 1;
       }
       // 递归写法,效率低
       // return (lastRemaining(n-1, m) + m) % n;
       int* result = new int[n];
       result[0] = 0;
       for(int i = 1; i < n; i++)
       {
           result[i] = (result[i-1] + m) % (i+1);
           // cout << i << " " << result[i] << endl;
       }
       return result[n-1];
   }
};

解题思路:
解法一:经典解法。用环形链表模拟操作过程。超时,心塞。注意m=1的情况。

class Solution {
public:
    struct ListNode{
        int val;
        ListNode* next;
        ListNode(int val) {this->val = val; next = NULL;}
    };
    int lastRemaining(int n, int m) {
        if (n == 0)
        {
            return 0;
        }
        m = m % n;
        ListNode* head = new ListNode(0);
        ListNode* tmp = head;
        // 构建一个环形链表
        for(int i = 1; i < n; i++)
        {
            ListNode* h = new ListNode(i);
            tmp->next = h;
            tmp = h;
        }
        tmp->next = head;
        /* 验证链表构建是否正确
        tmp = head;
        for(int i = 0; i < n + 1; i++)
        {
            cout << tmp->val << " ";
            tmp = tmp->next;
        }
        cout << endl;
        */
        if (m == 1)   // 一直在删除当前节点,剩余的一定是尾节点
        {
            return tmp->val;
        }
        tmp = head;
        while(tmp->next != tmp)
        {
            for(int i = 1; i < m - 1; i++)
            {
                tmp = tmp->next;
            }
            // cout << tmp->val << endl;
            ListNode* del = tmp->next;
            tmp->next = tmp->next->next;
            delete del;
            tmp = tmp->next;  
        }
        return tmp->val;
    }
};

解法二:找规律,(自己推一推)最终归纳出在n个数中删除第m个数的最终剩余元素的关系是
参考链接https://blog.csdn.net/u011500062/article/details/72855826
思路上就是,用f(n,m)代表n个人杀掉第m个人之后的幸存者下标。那个f(n-1,m)就是杀掉一个人之后,当前幸存者的下标移动到的位置。因为下一次计数是从被杀掉的下一个开始,因此就是从m+1开始记。因此幸存者的下标往前移动了m位。反推,则得到f(n,m) = f(n-1,m) + m。因为下标可能存在越界,因此更改为f(n,m)=[f(n-1,m) + m] % n。
过程

杀人过程图解

class Solution {
public:
    int lastRemaining(int n, int m) {
        if (n == 0)
        {
            return 0;
        }
        if (m == 1) // 总是删除当前节点,最后获得的一定是最后一个节点
        {
            return n - 1;
        }
        // 递归写法,效率低
        // return (lastRemaining(n-1, m) + m) % n;
        int* result = new int[n];
        result[0] = 0;
        for(int i = 1; i < n; i++)
        {
            result[i] = (result[i-1] + m) % (i+1);
            // cout << i << " " << result[i] << endl;
        }
        return result[n-1];
    }
};

你可能感兴趣的:(剑指offer学习笔记:6.4 抽象建模能力)