LeetCode 1900. The Earliest and Latest Rounds Where Players Compete O(1)贪心解法及其证明

题目链接:LeetCode 1900

首先,由于对称性,不妨假设firstPlayer < secondPlayer 且 firstPlayer < n - secondPlayer + 1。(firstPlayer == n - secondPlayer + 1的情况,则第一轮就会遇到)
官方按照secondPlayer 所处的三种位置的情况讨论,进行动态规划。事实上由于n比较小,也可以用状态压缩枚举胜负情况进行动态规划(见后面代码)。

贪心解法

下面讨论的是更好的方法,为贪心方法。原本贪心解法时间复杂度为O(logn),后面將其優 化成O(1),适用于n(long long范围)很大的情况。

将最少和最多轮数分别求解。为了方便讨论,下面的a表示的是firstPalyer,b是secondPlayer,m是中间位置(当人数为偶数时是虚拟位置)

主要思路:要让a,b相遇,则必须让两边的人数相等。要求最少的轮数,就是尽快的让两边数目相等,分几种情况进行讨论。 而为了让轮数最多,则优先淘汰人数少的以一边,让两边偏离平衡。

最少的轮数

  • 情形1: 当a和b分别位于m的两侧(不包括m) ,所有人的顺序如下所示
    (L)xxx a xxx b’ xxx m xxx b xxx a’ xxx (R)
    其中L是左端,R是右端,b’是b的对称点,a’是a的对称点,x是其他人

    • 情形1.1: 当La之间的数目是偶数(不含端点),或者ab’之间的数目(不含端点)不为0时,可以通过一轮使得a和b对称(相遇),所以这种情况最少通过2轮
      具体做法如下:当La为偶数,则让La中一半的胜利,让ba’中的人失败,这样La之间的数目就等于bR。当La为奇数,则让La中一半(一半向上取整)的人胜利,且让ab’的人中一个失败,其他胜利。则同样La中的人和bR中剩下的人数一样。
    • 情形1.2 当La之间的数目不是偶数且ab’之间数目为0,即ab’挨在一起。这时候顺序如下所示:
      (L)xxx ab’ xxx b a’ xxx (R)
      • 情形1.2.1 b’b挨在一起,此时n必为偶数且b’b位于中间(两者等价)。这时候经过一轮以后ab挨在一起(bb’第一轮淘汰掉b’),变成了情形2.1,所以总次数是n每次减半直到1的次数,为ceil(log(n) / log(2))。
      • 情形1.2.2 b’b不挨在一起,只需进行3轮(2轮以内不可能,所以必然3轮最少)。具体做法,La之间一半胜利(向下取整),则经过一次以后,就变成情形1.1。所以总共3轮
  • 情形2: 当a和b位于m左侧(包括m)
    为了使得a,b尽早相遇,就要尽量保留b前面的人,这是因为当a,b遇到时,剩下的人越多,说明他们遇到的轮数越早,而最后剩下的人数为b + a - 1,当淘汰a前面的人时,a,b都会减少,当淘汰b前面的人时,b会减少。所以无论什么情况,至少需要经过一定的轮数,使得当a + b > n才有可能让a,b相遇。设a + b > n的前一轮总人数为n_i个,则有 n_i >= a + b > (n_i + 1) / 2。下面证明当b != a + 1时,当到达n_i这一轮就能使ab相遇,而当b == a + 1时,则需要等到n第一次变成偶数。

    • 情形2.1 当ab挨着(b = a + 1),此时n_i >= 2a + 1 > (n_i + 1) / 2。由于ab挨着,所以必须是n < n_i的轮数,且n为偶数的轮才可能让ab对称。设n_j为第最先到达的轮使得n < n_i 且为偶数的n,则最终剩下的数目 n <= n_j。我们可以让n = n_j,所以n_j轮必然是最优的,做法是,在n_i轮,由于ab都在左侧(包含中间,由n_i >= 2a + 1得到),且两侧的人数差为n _i - (a + 1) - a = n_i - 2a - 1 < n_i / 2,所以可以控制两侧的人数差最多为1个。所以当最后一轮由奇数变成偶数的轮时,就可以让两侧相等。
    • 情形2.2 当ab不挨着,b > a + 1。此时(n_i + 1) / 2 > a,即在n_i这一轮a在左侧。在这一轮
      • 情形2.2.1 如果b在左侧(b <= ( n_i + 1)/ 2),则可以通过一轮,使他们相遇。假设右半边全部淘汰,则会导致bR <= La,如果等于,那么就结束了。如果不等于则只需让a’R中的(La - bR) / 2个胜利,b’a’中的 (La - bR) % 2个胜利,就会使得La = bR。
      • 情形2.2.2 如果b在右侧,则说明不是第一轮,所以在前一轮,可以让a,b之间的一个被淘汰,这样使得ab’不是挨着的。在当前轮(n_i这一轮),就会导致要么b是在左侧,或者依旧在右方。b在左侧的情况就是上一种情形,通过一轮使得他们相遇。而b在右方则为情形1.1,也是一样的。

最多的轮数

  • 情形1: a,b都位于左侧(包含中间)
    则可以通过一轮,将a,b变成1和2的位置。此时,必须所有人都淘汰完ab才能相遇,所以总轮数为ceil(log(n) / log(2)) (其实可以不用分情况1,情况2的讨论不仅适用于情形2,适用于所有情况)
  • 情形2: a,b位于两侧(不包含中间)。
    首先总次数不会大于ceil(log(n) / log(2)),另外总次数也不会大于n - b + 1,这是因为每一轮a肯定要干掉bR中的一个。可以证明最多次数可以达到ceil(log(n) / log(2))和n - b + 1之间的较小值。
    • 设 ceil(log(n) / log(2)) <= n - b + 1,则可以优先淘汰左半边使得轮数达到ceil(log(n) / log(2))。因为在淘汰过程中除最后一轮中间必然不可能使得ab相遇,第一轮a将变成位置1,如果ab相遇,则b为最后一个位置,由于我们优先淘汰前面部分,所以此时必有ceil(log(n) / log(2)) > n - b + 1,这和总次数可以达到ceil(log(n) / log(2))矛盾。
    • 设 ceil(log(n) / log(2)) > n - b + 1,则同样可以通过优先淘汰左半边来达到这个目的

优化前的代码(包括状态压缩动规划的解法)

/*
 * 1900. The Earliest and Latest Rounds Where Players Compete
 *  题意:有n个人进行淘汰,每次两两进行一次淘汰,第一个和最后一个,第二个和倒数第二个,以此类推。
 *      每两个人淘汰一个人,胜出的按原次序排成一列继续进行。现在有n个人,已知最强的人,和第二强的人的位置。 问这两个人最快第几轮可以遇到,最慢呢?
 *  题解:利用对称性,可以简化一下问题,将所有人翻转,以及调换最强和第二强的位置,结果不会改变,选择这几种等价结果情况中,最左端位置最小的。
 *      1.官方题解是枚举第二人的左中右三个位置进行动态规划。
 *        下面的代码采用另一种更为简单的办法,就是用状态压缩枚举每一轮的结果,讨论的情况会少很多,实现更为简单。递归进行动态规划。
 *      2.在题解中,发现一个更强的解法,就是贪心。在https://leetcode-cn.com/problems/the-earliest-and-latest-rounds-where-players-compete/solution/greedy-ologn-solution-by-wisdompeak-r28c/的基础上进一步优化,并对其正确性进行了证明。
 */


class Solution {
    // unordered_map<int , pair<int,int> > dp;

    // m <= 14, 2^4 = 16 > 14,用位移更快。
    // inline int encode(int l, int m, int r) {
    //     return (l << 9) + (r << 4) + m;
    // }
    // int getbits(int n) {
    //     n = (n & 0x55555555) + ((n >>1)  & 0x55555555) ; 
    //     n = (n & 0x33333333) + ((n >>2)  & 0x33333333) ; 
    //     n = (n & 0x0f0f0f0f) + ((n >>4)  & 0x0f0f0f0f) ; 
    //     n = (n & 0x00ff00ff) + ((n >>8)  & 0x00ff00ff) ; 
    //     // 本题 n 最多14位
    //     //n = (n & 0x0000ffff) + ((n >>16) & 0x0000ffff) ; 
    //     return n ;
    // }

   

    // pair<int, int> getdp(int n, int firstPlayer, int secondPlayer) {
        
    //     if(firstPlayer > secondPlayer) swap(firstPlayer, secondPlayer);
    //     // 保证等价的情况中firstPlayer最小
    //     if(firstPlayer > n - secondPlayer + 1) {
    //         int t = n - secondPlayer + 1;
    //         secondPlayer = n - firstPlayer + 1;
    //         firstPlayer = t;
    //     }
    //     pair<int,int> &ans = dp[encode(n, firstPlayer, secondPlayer)];
    //     if(ans.first != 0) return ans;
    //     if(firstPlayer == n - secondPlayer + 1) return {1, 1};
        
    //     ans.first = 0x3f3f3f3f;
    //     ans.second = 1;

    //     int r = (n + 1) / 2;
    //     int state = (1 << (firstPlayer - 1));
    //     if(secondPlayer <= r) {
    //         state |= (1 << (secondPlayer - 1));
    //     } else{
    //         state |= (1 << (n - secondPlayer));
    //     }
    //     //这一句可以不要
    //     state = (~state) & ((1 << r) - 1);
        
    //     for(int i = ((1 << r) - 1) & state; ; i = (i - 1) & state) {
    //         int f = getbits(i & ((1 << firstPlayer) - 1)) + 1;
    //         int s = -1;
    //         // 在左边 (包括中间)
    //         if(secondPlayer <= r) {
    //             s = getbits(i & ((1 << secondPlayer) - 1)) + 2;
    //         // 在右边
    //         } else{
    //             s = r - (n - secondPlayer) 
    //                 + getbits(i & ((1 << (n - secondPlayer)) - 1)) + 1;
    //         }

    //         pair<int,int> res = getdp(r, f, s);
    //         ans.first = min(ans.first, res.first);
    //         ans.second = max(ans.second, res.second);
    //         if(i == 0) break;
    //     }
    //     ++ans.first; ++ans.second;
    //     return ans;
    // }

    inline void simplify(int n, int &firstPlayer, int &secondPlayer) {
        if(firstPlayer > secondPlayer) swap(firstPlayer, secondPlayer);
        if(firstPlayer > n - secondPlayer + 1) {
            int t = n - secondPlayer + 1;
            secondPlayer = n - firstPlayer + 1;
            firstPlayer = t;
        }
    }

    int greedyMin(int n, int firstPlayer, int secondPlayer) {

        int r = (n + 1) >> 1;

        //情形1
        if(secondPlayer > r) {
            //情形1.1
            if((firstPlayer & 1) || (n != secondPlayer + firstPlayer)) {
                return 2;
            }
            //情形1.2.1
            if(!(n & 1)  && secondPlayer == r + 1) {
                return ceil(log(n) / log(2)); 
            }
             //情形1.2.2
            return 3;
        }
        
        //情形2
        int ans = 1;
        while(firstPlayer + secondPlayer <= n) {
            n = (n + 1) >> 1;
            ++ans;
        }
        if(secondPlayer - firstPlayer == 1) {
            while(n & 1) {
                n = (n + 1) >> 1;
                ++ans;
            }
        }
        return ans;
    }
    int greedyMax(int n, int firstPlayer, int secondPlayer) {

        int r = (n + 1) >> 1;
        // 情形1
        if(secondPlayer <= r) return ceil(log(n) / log(2));
        
        // 情形2
        int ans = 0;
        do{
            secondPlayer += r - n + 1;
            ++ans;
            n = r;
            r = (r + 1) >> 1;
        } while(secondPlayer > r && secondPlayer != n);
        if(secondPlayer == n) return ans + 1;
        return ans + ceil(log(n) / log(2)) ;
    }

public:
    vector<int> earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        // pair<int,int> ans = getdp(n, firstPlayer, secondPlayer);
        // return {ans.first, ans.second};
        simplify(n, firstPlayer, secondPlayer);
        if(firstPlayer == n - secondPlayer + 1) return {1, 1};

        return {greedyMin(n ,firstPlayer, secondPlayer), greedyMax(n ,firstPlayer, secondPlayer)};
    }
};

利用位运算和二进制的特征简化循环

简化1

在greedyMin中的情形2,原始代码为

		int ans = 1;
        while(firstPlayer + secondPlayer <= n) {
            n = (n + 1) >> 1;
            ++ans;
        }
        if(secondPlayer - firstPlayer == 1) {
            while(n & 1) {
                n = (n + 1) >> 1;
                ++ans;
            }
        }
        return ans;

n = (n + 1) / 2这个操作,可以转化成(n / 2) + (n & 1)。并且,连续k次操作,可以简化成
(n >> k) + ((n & ((1 << k) - 1)) > 0)
现在来求一下这循环

		while(firstPlayer + secondPlayer <= n) {
            n = (n + 1) >> 1;
            ++ans;
        }

执行的次数k。设s=firstPlayer + secondPlayer,问题就是求最小的k使得(n >> k) + ((n & ((1 << k) - 1)) > 0) < s。
此时有n >> k < s ,得到 n / (s - 1) <= 2^k,即
ceil(log( n / (s - 1)) / log(2)) <= k,由此得到k的一个下限ceil(log( n / (s - 1)) / log(2)),但是有可能 (n >> k) + ((n & ((1 << k) - 1)) > 0) == s,所以再进行一次判断,在k和k +1中选择符合条件的即可。

		int ans = ceil(log((n - 1) / (firstPlayer + secondPlayer - 1)) / log(2));

        if(n & ((1 << ans) - 1)) {
            n = (n >> ans) + 1;
        } else{
            n = (n >> ans);
        }
        
        if(firstPlayer + secondPlayer <= n) {
            ++ans;
            n = (n + 1) >> 1;
        }

简化2

下面这一部分循环

		if(secondPlayer - firstPlayer == 1) {
            while(n & 1) {
                n = (n + 1) >> 1;
                ++ans;
            }
        }

由上面可知k次减半操作等价于 n = (n >> k) + ((n & ((1 << k) - 1)) > 0),当n为奇数时,上面右边第二部分恒等于1。所以等价于n = n >> k + 1。问题就变成要找最小的k > 0,使得n >> k为奇数。假设n - 1的二进制为xxxxx 1 0…0,最低位的1位于第k位。则(n - 1) ^ (n - 2) = 11…1 = 2^(k+1) - 1。k = floor(log((n - 1)^(n - 2)) / log(2)) 。

因此上面循环可以直接简化成

		if(secondPlayer - firstPlayer == 1 && (n & 1)) {
            ans += log((n - 1) ^ (n - 2)) / log(2);
        }

简化3

在greedyMax中,情形2原始代码为

		int ans = 0;
        do{
            secondPlayer += r - n + 1;
            ++ans;
            n = r;
            r = (r + 1) >> 1;
        } while(secondPlayer > r && secondPlayer != n);
        if(secondPlayer == n) return ans + 1;
        return ans + ceil(log(n) / log(2)) ;

代码逻辑就是,不断优先淘汰前面的人,直到secondPlayer为最后一个,此时它们相遇,或者secondPlayer前面已经没有可以淘汰的了,此时转化为情形1。

每次优先淘汰前面的人时(b超过一半),会导致bR 之间的人减1(被a淘汰掉)。当遇到第一种情况时(第一个return),经过的轮数就是bR = n - secondPlayer + 1。当遇到第二种情况时,轮数就是ceil(log(n) / log(2)),只需判断两者谁比较小,就是哪种情况。再结合情况1,此时一定是ceil(log(n) / log(2))更小,所以两者取小值就是答案。

return min((int)ceil(log(n) / log(2)), n - secondPlayer + 1); 

优化后的代码

在本题n比较小的情况下,实际上比上面的代码还要慢。

class Solution {

    inline void simplify(int n, int &firstPlayer, int &secondPlayer) {
        if(firstPlayer > secondPlayer) swap(firstPlayer, secondPlayer);
        if(firstPlayer > n - secondPlayer + 1) {
            int t = n - secondPlayer + 1;
            secondPlayer = n - firstPlayer + 1;
            firstPlayer = t;
        }
    }

    int greedyMin(int n, int firstPlayer, int secondPlayer) {

        int r = (n + 1) >> 1;
        if(secondPlayer > r) {
            if((firstPlayer & 1) || (n != secondPlayer + firstPlayer)) {
                return 2;
            }
            if(!(n & 1)  && secondPlayer == r + 1) {
                return ceil(log(n) / log(2)); 
            }
            return 3;
        }
        
        int ans = ceil(log((n - 1) / (firstPlayer + secondPlayer - 1)) / log(2));

        if(n & ((1 << ans) - 1)) {
            n = (n >> ans) + 1;
        } else{
            n = (n >> ans);
        }
        
        if(firstPlayer + secondPlayer <= n) {
            ++ans;
            n = (n + 1) >> 1;
        }
        
        if(secondPlayer - firstPlayer == 1 && (n & 1)) {
            ans += log((n - 1) ^ (n - 2)) / log(2);
        }
        // 加上最后一次a,b之间的比赛
        return ans + 1;
    }
    inline int greedyMax(int n, int firstPlayer, int secondPlayer) {
        return min((int)ceil(log(n) / log(2)), n - secondPlayer + 1);
    }

public:
    vector<int> earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        simplify(n, firstPlayer, secondPlayer);
        if(firstPlayer == n - secondPlayer + 1) return {1, 1};

        return {greedyMin(n ,firstPlayer, secondPlayer), greedyMax(n ,firstPlayer, secondPlayer)};
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值