题目链接: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轮
- 情形1.1: 当La之间的数目是偶数(不含端点),或者ab’之间的数目(不含端点)不为0时,可以通过一轮使得a和b对称(相遇),所以这种情况最少通过2轮。
-
情形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)};
}
};