Monday, June 3, 2019

Leetcode 752 Open the Lock

题目原文

You have a lock in front of you with 4 circular wheels. Each wheel has 10 slots: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'. The wheels can rotate freely and wrap around: for example we can turn '9' to be '0', or '0' to be '9'. Each move consists of turning one wheel one slot.
The lock initially starts at '0000', a string representing the state of the 4 wheels.
You are given a list of deadends dead ends, meaning if the lock displays any of these codes, the wheels of the lock will stop turning and you will be unable to open it.
Given a target representing the value of the wheels that will unlock the lock, return the minimum total number of turns required to open the lock, or -1 if it is impossible.
Example 1:
Input: deadends = ["0201","0101","0102","1212","2002"], target = "0202"
Output: 6
Explanation:
A sequence of valid moves would be "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202".
Note that a sequence like "0000" -> "0001" -> "0002" -> "0102" -> "0202" would be invalid,
because the wheels of the lock become stuck after the display becomes the dead end "0102".
Example 2:
Input: deadends = ["8888"], target = "0009"
Output: 1
Explanation:
We can turn the last wheel in reverse to move from "0000" -> "0009".
Example 3:
Input: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
Output: -1
Explanation:
We can't reach the target without getting stuck.
Example 4:
Input: deadends = ["0000"], target = "8888"
Output: -1
Note:
  1. The length of deadends will be in the range [1, 500].
  2. target will not be in the list deadends.
  3. Every string in deadends and the string target will be a string of 4 digits from the 10,000 possibilities '0000' to '9999'.

题意分析

转自 : here
从初始状态“0000”开始转动,每次只能将其中一位转动一格,可正转可反转,同时deadends中有多个状态,如果转到该状态则不能继续,也即必不可转到目标结果。求问转到目标结果的最小次数。

解法分析

本题需要找到一个最小次数,通常需要运用BFS,在BFS中加入剪枝,并且对于到过的状态建立unordered_set,通过上述方法减少遍历次数。C++代码如下:
class Solution {
public:
   /* static bool isEnd(string a,vector<string>& deadends){
        for(auto k:deadends){
            if(a==k)
                return 1;
        }
        return 0;
    }*/
    int openLock(vector<string>& deadends, string target) {
        queue<string> myQue;
        unordered_set<string> dde(deadends.begin(),deadends.end());
        if(dde.count("0000"))
            return -1;
        myQue.push("0000");
        int queSize=1;
        int layer=0;
        int i,j;
        string temp,temp1;
        unordered_set<string> mySet;
        mySet.insert("0000");
        while(!myQue.empty()){
            layer++;
            queSize=myQue.size();
            for(i=0;i<queSize;i++){
                temp=myQue.front();
                for(j=0;j<4;j++){
                    temp1=temp;//add
                    if(temp1[j]=='9')
                        temp1[j]='0';
                    else
                        temp1[j]+=1;
                    if(temp1==target)
                        return layer;
                    if(!dde.count(temp1)&&!mySet.count(temp1)){            
                            myQue.push(temp1);
                            mySet.insert(temp1);                            
                    }
                    temp1=temp;//sub
                    if(temp1[j]=='0')
                        temp1[j]='9';
                    else
                        temp1[j]-=1;
                    if(temp1==target)
                        return layer;
                    if(!dde.count(temp1)&&!mySet.count(temp1)){
                            myQue.push(temp1);
                            mySet.insert(temp1);                   
                    }
                }
                myQue.pop(); 
            }
        }
        return -1;  
    }
};
上述代码不超时的关键除了剪枝和记录去过的状态以外,还在于检测一个状态是否deadend的方法,如果采用每次都遍历deadend一遍的方法则会超时,如果将deadend做成一个unordered_set,则可以是查询复杂度从O(n)降到O(1)。


Sunday, June 2, 2019

Leetcode 718 Maximum Length of Repeated Subarray

题目原文

Given two integer arrays A and B, return the maximum length of an subarray that appears in both arrays.
Example 1:
Input:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
Output: 3
Explanation: 
The repeated subarray with maximum length is [3, 2, 1].
Note:
  1. 1 <= len(A), len(B) <= 1000
  2. 0 <= A[i], B[i] < 100
转自 : here

题意分析

给定两个长度不一定相等的整型数组,找出他们间相同子数组的最大长度。

解法分析

此题采用动态规划方法,关键是找到最优子结构,首先我想到的是用bothLen[i][j]表示以A[i],B[j]结尾的数组的相同子数组的最长长度,但是这样做不能得到最优子结构,因此用bothLen[i][j]表示以A[i],B[j]结尾的相同子数组的长度,bothLen[i][j]=(A[i]==B[j])?bothLen[i-1][j-1]+1:0,C++代码如下:
class Solution {
public:
    int findLength(vector<int>& A, vector<int>& B) {
        vector<vector<int>> bothLen(A.size()+1,vector<int>(B.size()+1));
        int maxLen=0;
        for(int i=1;i<=A.size();i++){
            for(int j=1;j<=B.size();j++){
                bothLen[i][j]=(A[i-1]==B[j-1])?bothLen[i-1][j-1]+1:0;
                maxLen=max(bothLen[i][j],maxLen);
            }     
        }
        return maxLen;  
    }
};
为了减少空间消耗,可以将二维vector转化为一维,但是j的遍历方向需要改变。
class Solution {
public:
    int findLength(vector<int>& A, vector<int>& B) {
        vector<int> bothLen(B.size()+1,0);
        int maxLen=0;
        for(int i=1;i<=A.size();i++){
            for(int j=B.size();j>0;j--){
                bothLen[j]=(A[i-1]==B[j-1])?bothLen[j-1]+1:0;
                maxLen=max(bothLen[j],maxLen);
            }     
        }
        return maxLen;  
    }
};
这里bothLen[j]表示了bothLen[j][i],bothLen[j-1]表示了bothLen[j-1][i-1],为了达到这个效果j需要从大往小变,以免覆盖i-1时的值。因此对于一维数组的递归式,如a[i]=a[i-1]+1,这种可以用一个变量表示,而a[i]=a[i-1]+a[i-2],可以用两个变量表示,二维如a[i][j]=a[i][j-1]可以用a[i]表示,而本题这种需要通过改变j的遍历方向减少所需空间。当然也可以将将二维a[i][j]变为a[2][j],第一维大小固定为2.

Saturday, June 1, 2019

[LeetCode] Game of Life 生命游戏

 solve it in-place

from here

每一个位置有两种状态,1为活细胞,0为死细胞;现在我们可以丰富每一个位置上的信息,让它能表示四种状态:
状态0: 死细胞转为死细胞
状态1: 活细胞转为活细胞
状态2: 活细胞转为死细胞
状态3: 死细胞转为活细胞

最后我们对所有状态对2取余,那么状态0和2就变成死细胞,状态1和3就是活细胞,这样可以获得一次转化的结果。


class Solution {
public:
    void gameOfLife(vector<vector<int> >& board) {
        int m = board.size(), n = m ? board[0].size() : 0;
        int dx[] = {-1, -1, -1, 0, 1, 1, 1, 0};
        int dy[] = {-1, 0, 1, 1, 1, 0, -1, -1};
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                int cnt = 0;
                for (int k = 0; k < 8; ++k) {
                    int x = i + dx[k], y = j + dy[k];
                    if (x >= 0 && x < m && y >= 0 && y < n && (board[x][y] == 1 || board[x][y] == 2)) {
                        ++cnt;
                    }
                }
                if (board[i][j] && (cnt < 2 || cnt > 3)) board[i][j] = 2;
                else if (!board[i][j] && cnt == 3) board[i][j] = 3;
            }
        }
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                board[i][j] %= 2;
            }
        }
    }
};

Leetcode 121 Best Time to Buy and Sell Stock

题目原文

Say you have an array for which the ith element is the price of a given stock on day i.
If you were only permitted to complete at most one transaction (ie, buy one and sell one share of the stock), design an algorithm to find the maximum profit.
Example 1:
Input: [7, 1, 5, 3, 6, 4]
Output: 5

max. difference = 6-1 = 5 (not 7-1 = 6, as selling price needs to be larger than buying price)
Example 2:
Input: [7, 6, 4, 3, 1]
Output: 0

In this case, no transaction is done, i.e. max profit = 0.

转自:here

题意分析

本题是一个简单的求最优解问题,只能买卖一次股票,求最大收益,当卖的时候的价格比买的时候高,收益为0。

解法分析

本题采用动态规划方法求解。如果用暴力方法,显而易见算法复杂度为O(n^2),所以一般用动态规划该方法复杂度变为o(n),只有一个for循环,也就是通过遍历一次原序列就能得到解。首先分析最优子结构,令从左开始的一个子序列中最小元素为a,下一个元素为b,如果b比a小,则算上b后的序列的最小元素就是b,否则仍是a;如果b比a大,则求其差值,与Pro比较,如果差值大于pro,则修改pro的值,否则pro的值不变。这是一个简单的自底向上的动态规划问题。C++代码如下:
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n=prices.size();
        if(n==0||n==1)
            return 0;
        int pro=0,min=prices[0],i,temp;
        for(i=1;i<=n-1;i++){
            if(prices[i]>min){
                temp=prices[i]-min;
                if(temp>pro)
                    pro=temp;
            }
            else
                min=prices[i];
        }
        return pro;   
    }
};
由于只有一个for循环,所以复杂度为O(n),注意对空输入和只有一个元素的输入的处理,都应输出0。

Another way, from here


这道题求进行一次交易能得到的最大利润。如果用brute force的解法就是对每组交易都看一下利润,取其中最大的,总用有n*(n-1)/2个可能交易,所以复杂度是O(n^2)。
很容易感觉出来这是动态规划的题目,其实跟Maximum Subarray非常类似,用“局部最优和全局最优解法”。思路是维护两个变量,一个是到目前为止最好的交易,另一个是在当前一天卖出的最佳交易(也就是局部最优)。递推式是local[i+1]=max(local[i]+prices[i+1]-price[i],0), global[i+1]=max(local[i+1],global[i])。这样一次扫描就可以得到结果,时间复杂度是O(n)。而空间只需要两个变量,即O(1)。代码如下:
public int maxProfit(int[] prices) {
    if(prices==null || prices.length==0)
        return 0;
    int local = 0;
    int global = 0;
    for(int i=0;i<prices.length-1;i++)
    {
        local = Math.max(local+prices[i+1]-prices[i],0);
        global = Math.max(local, global);
    }
    return global;
}
这种题目的解法非常经典,不过是比较简单的动态规划。这道题目还有两个变体,Best Time to Buy and Sell Stock IIBest Time to Buy and Sell Stock III,虽然题目要求比较像,但是思路却变化比较大,Best Time to Buy and Sell Stock II可以交易无穷多次,思路还是比较不一样,而Best Time to Buy and Sell Stock III则限定这道题交易两次,其实还可以general到限定交易k次,会在这道题目中仔细讲解,有兴趣的朋友可以看看哈。

Leetcode 122 Best Time to Buy and Sell Stock II

题目原文

Say you have an array for which the ith element is the price of a given stock on dayi.
Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times). However, you may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

转自:here

题意分析

本题与121题同为买卖问题,不同的是121只能买卖一次,而本题可以买卖多次,但每次买必须在卖过之后。121题采用简单的自底向上动态规划方法解,本题将采用贪心策略。

解法分析

本题是一个最优化问题,采用动态规划方法有点杀鸡焉用宰牛刀,本题我采用贪心策略。使用贪心策略要考虑两点,一是贪心选择,二是最优子结构。首先看贪心选择,这里贪心选择为一次贪心买卖。对于买入的时机,如果下一天的值更小,则选择下一天买,这样肯定能比在前一天买获得更多收益,如果后一天的值大于前一天买入值了,则开始考虑是否卖出,遍历剩下的天数,直到数值开始下降,在下降的前一天卖出,这样一次贪心的买卖完成,剩下的天数为子问题,具有最优子结构。C++代码如下:
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int i;
        auto n=prices.size();
        if(n==0||n==1)
            return 0;
        int maxPro=0;
        int buy=prices[0],sell;
        for(i=0;i<n;i++){
            buy=prices[i];
            if(prices[i+1]<=buy)
                buy=prices[i+1];  
            else{
                while((prices[i]<=prices[i+1])&&(i+1)<n)
                    i++;
                sell=prices[i];
                maxPro+=(sell-buy);
            }   
        }
        return maxPro;      
    }
};
算法复杂度为O(n),用buy和sell来保存每次买卖的价格。

Thursday, May 30, 2019

Best Time to Buy and Sell Stock IV

另一种动态规划解法:(参考 herehere)

buy[i][j]:第i天处于买入股票状态,同时从开始到今天可最多进行j次交易时的最大利润
sold[i][j]:第i天处于卖出股票状态,同时从开始到今天可最多进行j次交易时的最大利润

对于buy[i][j],它的更新有两种情况:
一种是第i天没有买入股票,则buy[i][j]=buy[i-1][j]
一种是第i天买入股票,buy[i][j]=sold[i-1][j-1]-prices[i]

因此,
buy[i][j]=max(buy[i-1][j], sold[i-1][j-1]-prices[i])

对于sold[i][j],它的更新有两种情况:
一种是第i天没有卖出股票,则sold[i][j]=sold[i-1][j]
一种是第i天有卖出股票,sold[i][j]=buy[i-1][j]+prices[i]

因此,
sold[i][j]=max(sold[i-1][j], buy[i-1][j]+prices[i])

因此总的状态方程为:
buy[i][j]=max(buy[i-1][j], sold[i-1][j-1]-prices[i])
sold[i][j]=max(sold[i-1][j], buy[i-1][j]+prices[i])


发现什么问题了没有?
第 i 天的结果只与第 i - 1 天的结果有关,与 i - 1 之前的天数完全无关!
既然如此,更新只需要用到前一天的数据,可以考虑转为一维dp:

buy[j]=max(buy[j], sold[j-1]-prices[i])
sold[j]=max(sold[j], buy[j]+prices[i])


注意注意,前方高能!
我们看到第 i 天的buy[j]需要用到前一天的sold[j-1],但是如果我们从j=0开始遍历的话,此时得到的sell[j-1]已经变成了第 i 天的sold[j-1]了啊。

解决方法
从j=k开始遍历到1,那么修改buy[j]和sold[j]之时,就能保证sold[j-1]还是前一天的值
---------------------

同是在计算sold[j]时,我们需要用到buy[i-1][j],但是转为一维dp后,因为我们先计算了buy[j],所以此时的 buy[j] 实际上是 buy[i][j],这样就与我们需要的不符。

解决办法:在更新buy[j]前,把它的值先保存到一个临时变量中。
但是在测试中发现,不用这样做,也不影响结果。原因分析如下
1)如果buy[i][j]==buy[i-1][j],则这样当然不会影响结果。
2)如果buy[i][j]==sold[i-1][j-1]-prices[i],这意味着
sold[i-1][j-1]-prices[i]>=buy[i-1][j]
<=>  sold[i-1][j-1]>=buy[i-1][j]+prices[i]   (*)
又因为
sold[i][j]=max(sold[i-1][j], buy[i-1][j]+prices[i]),
 那么,如果用 buy[i][j]来代替 buy[i-1][j]计算,buy[i-1][j]+prices[i]=sold[i-1][j-1]-prices[i]+prices[i]=sold[i-1][j-1],
显然 sold[i-1][j]>=sold[i-1][j-1]
根据(*), sold[i-1][j-1]>=buy[i-1][j]+prices[i]
这就是说,不论 buy[j] 值正确与否,sold[i][j]总是等于sold[i-1][j],故也不会影响结果。


另外一种方法是把sold在buy前面计算:
总的状态方程为:
sold[i][j]=max(sold[i-1][j], buy[i-1][j]+prices[i])
buy[i][j]=max(buy[i-1][j], sold[i-1][j-1]-prices[i])

==》
sold[j]=max(sold[j], buy[j]+prices[i])
buy[j]=max(buy[j], sold[j-1]-prices[i])


    int maxProfit(int k, vector<int>& prices) {
        if(prices.size() ==0) return 0;
        int len = prices.size(), ans =0;
        if(k >= len/2)
        {
            for(int i = 1; i < len; i++)
                if((prices[i]-prices[i-1])>0) ans += prices[i]-prices[i-1];
            return ans;
        }
        vector<int> buy(len+1, INT_MIN), sold(len+1, 0);
        for(auto val: prices)
        {
            for(int j =k; j >0; j--)
            {
                int tmp =buy[j];
                buy[j] = max(sold[j-1]-val, buy[j]);
                sold[j] = max(tmp+val, sold[j]);
            }
        }
        return sold[k];
    }



Leetcode 714 Best Time to Buy and Sell Stock with Transaction Fee

题目原文
Your are given an array of integers prices, for which the i-th element is the price of a given stock on day i; and a non-negative integer fee representing a transaction fee.

You may complete as many transactions as you like, but you need to pay the transaction fee for each transaction. You may not buy more than 1 share of a stock at a time (ie. you must sell the stock share before you buy again.)

Return the maximum profit you can make.

Example 1:

Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
Output: 8
Explanation: The maximum profit can be achieved by:
Buying at prices[0] = 1Selling at prices[3] = 8Buying at prices[4] = 4Selling at prices[5] = 9The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
Note:

0 < prices.length <= 50000.0 < prices[i] < 50000.0 <= fee < 50000.
题意分析
给定一组某一stock在每一天的价格,买卖次数不限,每次买入必须在卖出之后,且每次卖出时都需要fee的手续费,求解最大的收益。

解法分析 from here
本题采用两种方法解

动态规划
贪心算法
动态规划

对于第i天的最大收益,应分成两种情况,一是该天结束后手里没有stock,可能是保持前一天的状态也可能是今天卖出了,此时令收益为cash;二是该天结束后手中有一个stock,可能是保持前一天的状态,也可能是今天买入了,用hold表示。由于第i天的情况只和i-1天有关,所以用两个变量cash和hold就可以,不需要用数组。C++代码如下:

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int cash=0;//the maxPro you have if you don't have a stock that day
        int hold=-prices[0];//the maxPro you have if you have a stock that day, if you have a stock
                                     // the  first day,hold=-prices[0]
        int i;
        for(i=1;i<prices.size();i++){
            cash=max(cash,hold+prices[i]-fee);//cash in day i is the maxvalue of cash in day i-1 or you //sell your stock
            hold=max(hold,cash-prices[i]);
        }
        return cash;   
    }
};
这里需要注意cash和hold的初始值,最终输出cash,因为最后一天的情况一定是手里没有stock的。
贪心算法

贪心选择的关键是找到一个最大后是不是能够卖掉stock,重新开始寻找买入机会。比如序列1 3 2 8,如果发现2小于3就完成交易买1卖3,此时由于fee=2,(3-1-fee)+(8-2-fee)<(8-1-fee),所以说明卖早了,令max是当前最大price,当(max-price[i]>=fee)时可以在max处卖出,且不会存在卖早的情况,再从i开始重新寻找买入机会。c++代码如下:

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int profit=0;
        int curProfit=0;
        int minP=prices[0];
        int maxP=prices[0];
        int i;
        for(i=1;i<prices.size();i++){
            minP=min(minP,prices[i]);
            maxP=max(maxP,prices[i]);
            curProfit=max(curProfit,prices[i]-minP-fee);
            if((maxP-prices[i])>=fee){//can just sell the stock at maxP day.
                profit+=curProfit;
                curProfit=0;
                minP=prices[i];
                maxP=prices[i];
            }
        }
        return profit+curProfit;//the last trade have to be made if there is some profit
    }
};
curProfit记录了当前一次交易能得到的最大收益,只有当maxP-prices[i]>=fee时,才将curProfit累加到总的收益中。最后一次交易不需要考虑是否早卖了,所以直接累加最后一次的curProfit。


---------------------
作者:Zarlove
来源:CSDN
原文:https://blog.csdn.net/zarlove/article/details/78323469
版权声明:本文为博主原创文章,转载请附上博文链接!

Saturday, May 18, 2019

约瑟夫问题







约瑟夫问题的两个O(log n)解法


转自:http://maskray.me/blog/2013-08-27-josephus-problem-two-log-n-solutions
(同时添加了自己的一些心得)

这个是学习编程时的一个耳熟能详的问题了:
n个人(编号为0,1,...,n-1)围成一个圈子,从0号开始依次报数,每数到第m个人,这个人就得自杀,之后从下个人开始继续报数,直到所有人都死亡为止。问最后一个死的人的编号(其实看到别人都死了之后最后剩下的人可以选择不自杀……)。
这个问题一般有两种问法:
  • 给出自杀顺序。不少数据结构初学书都会以这个问题为习题考验读者对线性表的掌握。比较常见的解法是把所有存活的人组织成一个循环链表,这样做时间复杂度是O(n*m)的。另外可以使用order statistic tree(支持查询第k小的元素以及询问元素的排名)优化到O(n log n)。另外有篇1983年的论文An O(n log m) Algorithm for the Josephus Problem,但可惜我没找到下载链接。
  • 求出最后一个人的编号。可以套用上一种问法的解法,但另外有更加高效的解法,下文展开讨论。

时间O(n),空间O(1)

f(n)为初始有n人时最后一个自杀的人的编号,那么有如下递推式:





1
f(n) = (f(n-1) + m) mod n

n=5, m=3为例,一开始有这么5个人:





1
0 1 2 3 4

第一轮报数后2号死亡,圈子里剩下来的人的编号是这些:





1
3 4 0 1

这里把3号写在最前面了,因为轮到3开始报数。如果我们有办法知道n=4时的答案,即初始圈子为:





1
0 1 2 3

时的答案,那么可以把n=4的初始圈子的所有数x变换成(x+3) mod 5,得到:





1
3 4 0 1

这个恰好就是n=5时第一轮结束后的圈子状态,也就是说我们可以根据n=4的答案推出n=5时的答案。
手工演算一下就能发现n=z时的圈子第一轮结束后(即m-1号自杀后)的圈子状态,可以由n=z-1的初始圈子施以变换x -> (x+m) mod z得到。于是我们可以从n=1开始(此时的答案是0),推出n=2的答案,再推出n=3,直到计算到所要求的n
下面是C语言实现:





1
2
3
4
5
6
7
int f(int n, int m)
{
int s = 0;
for (int i = 2; i <= n; i++)
s = (s + m) % i;
return s;
}

时间O(log n),空间O(log n)

换一个递推思路,考虑报数过程又回到第0个人时会发生什么。这时有floor(n/m)*m个人都已经报过数了,并且死了floor(n/m)人。之后一轮的报数会数过m个人,又会回到第0个人。
我们以n=8, m=3为例看看会发生什么。一开始:





1
0 1 2 3 4 5 6 7

floor(n/3)*3个人都报过数后:





1
0 1 x 3 4 x 6 7 (A)

即2号和5号死亡,接下来轮到6号开始报数。因为还剩下6个人,我们设法做一个变换把编号都转换到0~5





1
2
2 3 x 4 5 x 0 1 (B)
___

(B)的答案等于规模缩小后的情况n'=6时最后一个人的编号s。然后根据s算出圈子状态(A)的最后一个人的编号。
如果s在最后一个x后面(带下划线),即s < n%m,那么s2 = s-n%m+n;否则s2 = s-n%m+(s-n%m)/(m-1)
注意如果n < m,那么报数回到第一个人时还没死过人。因为子问题的规模没有减小,所以上述方法不适用。需要用之前时间复杂度O(n)的方法递推。下面是C语言实现:





1
2
3
4
5
6
7
int rec(int n, int m)
{
if (n == 1) return 0;
if (n < m) return (rec(n - 1, m) + m) % n;
int s = rec(n - n / m, m) - n % m;
return s < 0 ? s + n : s + s / (m-1);
}

n每次变为n * (m-1) / m,即以一个常数因子衰减到刚好小于m,然后换用线性复杂度的递推算法,总的时间复杂度为O(m + log_{m/(m-1)}{n/m}),如果把m看作常数的话就是O(log n)。程序中在子问题计算后还需要做一些处理,无法尾递归,所以空间复杂度也等于这个。
参考:

另一种说明方式, 转自 https://zh.wikipedia.org/wiki/%E7%BA%A6%E7%91%9F%E5%A4%AB%E6%96%AF%E9%97%AE%E9%A2%98


对于,可以将上述方法推广,将杀掉第k2k、……、个人视为一个步骤,然后把号码改变,可得如下递推公式, , 运行时间为


这里的第三个公式,把s < n%m 与 s >= n%m,两种情况综合了起来。(公式中的k即为m)
为了理解这个公式,需要注意两个事实:

其一,下面例子中m=3,即每三个删除一个,然后从新标号得到第二行。(这里不是用约瑟夫游戏


1
2
0 1 2 3 4 5 6 7 8 9 10 11 12 13
0 1 2 3 4 5 6 7 8 9

要从第二行的数得到与之对应的第一行的数,可以发现第一行的数是每三个一组,而第二行的数是每两个一组。假设第二行的一个数为v,那么v/(m-1) 可得到组号,再乘以m,即m*v/(m-1)可得到之对应的第一行的数的大概位置。然后再验算一下,发现这个结果好于预期,居然可以算出第一行的数的精确位置!

其二,约瑟夫游戏时,第二行是按递归考虑时的重新编号,

1
2
0 1 2 3 4 5 6 7 8 9 10 11 12 13
2 3 4 5 6 7 8 9 0 1

我们的目的是要从第二行的数得到与之对应的第一行的数,这是我们可以利用其一中的事实了,只要我们把第二行的数转为其一中的第二行的数就行了,即:

1
2
0 1 2 3 4 5 6 7 8 9
2 3 4 5 6 7 8 9 0 1

公式为 (v-2)%n, 这里n=10

-----------------------------------------------------

把上面的递归公式转为代码后,发现结果不正确,会返回一个负数。经检查,问题出在
(f(n',k)- n%k) mod n' 上。原来c++的模运算和数学意义上的还是有些差别。


std::cout << (-7 % 3) << std::endl;
std::cout << (7 % -3) << std::endl;
give different results:
-1
1
解释:
From ISO14882:2011(e) 5.6-4:
The binary / operator yields the quotient, and the binary % operator yields the remainder from the division of the first expression by the second. If the second operand of / or % is zero the behavior is undefined. For integral operands the / operator yields the algebraic quotient with any fractional part discarded; if the quotient a/b is representable in the type of the result, (a/b)*b + a%b is equal to a.
The rest is basic math:
(-7/3) => -2
-2 * 3 => -6
so a%b => -1

(7/-3) => -2
-2 * -3 => 6
so a%b => 1
Note that
If both operands are nonnegative then the remainder is nonnegative; if not, the sign of the remainder is implementation-defined.

就是说当f(n',k)- n%k 为负值时,(f(n',k)- n%k) mod n' 也为负。解决这个问题只需在代码中用(f(n',k)- n%k+n') mod n' 就可以了。


时间O(log n),空间O(1)

三年前我还看到过一段更巧妙的代码,具体写法不可考了,当时盯着式子思考了好久呢。其等价的形式长这样:





1
2
3
4
5
6
int kth(int n, int m, int k)
{
if (m == 1) return n-1;
for (k = k*m+m-1; k >= n; k = k-n+(k-n)/(m-1));
return k;
}

这个函数解决了第二种问法的一个扩展问题,即第k个(从0数起)自杀的人的编号。如果取k = n-1那么就得到了最后一个自杀的人的编号。
这个算法的思想是第k*m+m-1次报数的人就是第k个自杀的人,追踪他之前每次报数的时刻来确定他的编号。
考虑n=5, m=3, k=4,一共有15次报数(每报m次数死一个人,一共有k+1个人要死,所以需要(k+1)*m次报数),每一次报数的人的编号如下:





1
2
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
0 1 2 3 4 0 1 3 4 1 3 1 3 3 3

报到2、5、8、11、14的人自杀。
设第p次(从0数起)报数的人是y,令p = m*a+b (0 <= b < m)
经过前p次报数,一共死了floor(p/m) = a人,圈子里还剩下n-a人。
如果y本次报数后没有自杀,那么他下次报数是在圈子里其他n-a人都报数之后,
也就是第q = p+n-a = n+(m-1)*a+b次报数。
这是后继的计算式,逆用即可计算前驱。
y本次报数后还存活知b < m-1,故a = floor((q-n)/(m-1)),【注】
p = q-(n-a) = q-n+floor((q-n)/(m-1))
我们要求第k个自杀的人,他是第(k+1)*m-1次报数后自杀的,用计算前驱的方式求出这个人之前一次报数是在什么时候,不断地重复这一过程,直到知道他是第k' (0 <= k' < n)次报数的人,那么k'就是这个人的编号。

【注】对于a,b,c三个数,其中b,c确定为正,这里需要证明:
if b<c, then (a-b)/c == a/c (注意,这里不考虑小数部分)
证明:
let a-b = k*c+x,     0<=x<c
let a= k'*c+y,  0<=y<c
then
k*c+x+b=k'*c+y  then,
(k-k')c=y-b-x
here,  -b<=y-b<c-b,   -c<-x<=0, then
-2c<-b-c<y-b-x<c-b<c
but (k-k')c must be a multiple of c, so y-b-x must be 0, that means k==k'

不知道还有没有更简洁的证明方法。