leetcode第20场双周赛总结

leetcode第二十场双周赛
第一次参加leetcode的双周赛,一共4道题做出来两道。

第一题很简单,就是写一个sort函数的排序规则cmp函数就行了,当然,涉及到一个基础的知识点:统计一个数的二进制表示中1的个数。看了一下题解,发现大家基本上都是用:num = num & (num - 1)来做的,这个操作可以使得num最低位的1变成0,因此只要while num不等于0时一直执行就可以了,而我是采用不断右移的方法,可能稍微慢一些,但是效果是一样的。要是以前的我,可能就会不断地“除2取余“去算了。题目中如果出现了“二进制”,那使用到位运算的可能性很大。
这道题写的时候遇到了编译错误,cmp函数如果写在外面的话,是要写成static的,否则,sort函数是看不到的。另一种方式是采用C++ 11 的标准,写成sort(arr.begin(), arr.end(), [&](int a, int b){})的形式,把函数体直接写在函数调用里面。

第二题,虽然难度标的是medium,但是也很简单,就是实现一个类的两个方法。这类题往往题面非常复杂,但是实际上实现的功能很简单。通过这道题熟悉了一下C++的类的编写方式。

第三题,我的思路一开始就错了,因为刚刚复习了动态规划,便觉得这道题是动态规划来做。实际上确实是可以的,但是比较难想。这里说一种双指针的做法。我其实也意识到了这是一道滑动窗口题,但是对这类题型还是不太熟悉,所以没想到窗口收缩和扩张的条件是什么。现在明白了,并且总结出一个“规律”:窗口的右端点增加往往意味着“妥协”,即某个条件不满足,所以必须要扩大窗口以寻求满足条件的解;而窗口的左端点增加往往是当已经满足了某个条件了(即右端点不需要增加)的情况下,进一步深挖看看在当前的右端点所处位置的条件下,有没有可能有更多的解。同时,使用哈希表来记录滑动窗口内各个字符的个数这样,你来我往,直到右端点走到序列的末尾,就结束了,这样的时间复杂度是O(n)。

第四题,是一道组合数学的题,涉及到高中数学中排列组合的知识点。这里讲两种解法,一种是递归,即“自顶向下”的写法;另一种是动态规划,即“自底向上”的解法。

递归

这种思路是把这个问题看成将n对数,即2n个数,按照一定要求放置在2n个位置上。回忆回忆高中数学解较复杂的排列组合题的时候,是不是经常涉及到分类讨论的思想?下面就按照“选位置”的思路分类讨论:

1. 有某个P选择了倒数第二个位置

由于P一定在对应的D前面,因此如果有P选了倒数第二个位置,那么对应的D一定是倒数第一个即最后一个位置,然后问题就转化为剩下n-1对订单在2n-2个位置中选位置的问题。那么,哪个P选了倒数第二个位置呢?有n种可能。所以,这种情况下共有n * f(n-1)种组合,其中f(n)代表当有n对订单时问题的解。

2. 没有P选择倒数第二个位置

那么,这时倒数第二个位置上一定是D,哪个D?有n种可能;而且,最后一个位置也一定是D,因为如果是P的话,这个P对应的D一定在它前面,与题意不符。这回又是哪个D?有n-1种可能。这时前面还剩下2*n-2个位置,这两个D对应的两个P可以在这2*n-2个位置中随便选两个位置来排列,就是A(2*n-2, 2),其中A代表排列数。此时,还剩下n-2对订单没有选位置,注意,由于订单之间是没有顺序的,所以不必在意前面两对订单具体选了什么位置,剩下的问题就是一个规模为n-2的问题,即f(n-2)。综上,这种情况下共有n*(n-1)*A(2*n-2, 2)*f(n-2)。

把两种情况加在一起,就是最终的答案。但是要注意答案可能很大,因此,在中间结果的时候就要对最大值10^9+7取余,免得中途溢出,除余运算公式:
(a+b)%c = (a % c) + (b % c);
(a * b * c) % d = (a %d) * (b %d) * (c %d)

动态规划

可以看到,上面递归的方法一开始就是从n对订单考虑的,然后缩小到n-1, n-2,这是自顶向下的思想。而动态规划的思路与之相反,它研究更大的问题规模如何有更小的问题递推出来,即“自底向上”。如本题中,边界是当没有或只有1笔订单时,有效的序列个数为1,即dp[0] = dp[1] = 1;那么每增加1笔订单时,如dp[n]是如何有dp[n-1]推出来的呢?
这时可以利用“插空法”。现在已经有2*(n-1)个位置上已经坐满了对应的P和D,那么新来一对P和D,我们要把他们插在合适的位置上,共有2*(n-1)+1=2*n-1个空位。这里还是利用分类讨论的思想便于理解,并且还是从最后一个位置开始下手:

新D在最后

在dp[n-1]的基础上,如果新来的D选择了最后一个空位(其实这里说“选空位”严格意义上已经不成立了,因为D选择最后之后,对应P可以选择的位置并没有变成2*n-1-1=2*n-2个,而仍然是2*n-1,因为P还是可以在D前面的,相当于又“冒出来”一个座位),那么P只能在它前面的空位进行选择,共有2*n-1种选择,因此这种情况下共有(2*n-1)*dp[n-1]种组合

新D不在最后

新D不在最后一个空位的话,那意味着之前谁坐在最后,谁就还坐在最后,新D可以在前面的2*n-1-1=2*n-2个位置中选择,注意,这时P有几种选择就跟D选择坐在“前面”还是“后面”密切相关了,D越坐在前面,P的选择越少,因为毕竟P只能坐在D的前面嘛,观察可以发现,P共有1+2+3+…+2*n-2=(2*n-1)*(n-1)种选择,最后别忘了乘上dp[n-1],毕竟我们的选择都是在前n-1笔订单已经坐好位置的前提下进行的嘛。
把两种情况加在一起,就是最终的答案。同样要注意溢出的问题,首先要定义long long类型,并且能多取几次余就取吧,没坏处,由除余公式保证正确就行了。
本题的代码贴在后面:
递归方法:

class Solution {
public:
    const int maxn = 1000000007;
    long long ready[510] = {0};
    int countOrders(int n) {
        return (int)run(n);
    }
    long long run(long long n){
        if(n == 0 || n == 1)    return 1;
        if(ready[n] != 0)   return ready[n];
        long long temp1 = run(n-1) % maxn;
        long long temp2 = (n * (n-1) * A(2*n-2, 2)) % maxn;
        ready[n] = (n * temp1 % maxn + (temp2 * (run(n-2) % maxn) % maxn)) % maxn;  
        return ready[n];
    }
    long long A(int m, int n){
        long long ans = 1;
        for(int i=0; i<n; i++){
            ans *= m;
            m--;
        }
        return ans;
    }
};

动态规划方法:

class Solution {
public:
    const int maxn = 1000000007;
    int countOrders(int n) {
        long long dp[510];
        dp[0] = dp[1] = 1;
        for(int i=2; i<=n; i++){
            dp[i] = (((2*i-1) % maxn) * (dp[i-1] % maxn)) % maxn + ((((2*i-1) * (i-1)) % maxn) * (dp[i-1] % maxn)) % maxn;
        }
        return (int)(dp[n] % maxn);
    }
};

你可能感兴趣的:(leetcode,算法,数据结构,面试,c++)