【问题标题】:Finding least number of moves寻找最少的移动次数
【发布时间】:2014-04-21 01:23:39
【问题描述】:

我有以下问题陈述:

给定一个数 n (1

到目前为止,我已经编写了以下代码以尝试解决该问题:

while(n!=1){

    if(n%3==0 || n%2==0){
        if(n%3==0){
            n=n/3;
            c=c+1;
        }
        if(n%2==0){
        n=n/2;
        c=c+1;
        }
    }
    else{
        n=n-1;
        c=c+1;
    }
}
System.out.println(c);

但我没有得到想要的输出。谁能帮帮我。

【问题讨论】:

  • 问题不在于代码,而在于逻辑。修正你的逻辑。
  • 增加了n的范围
  • 你能帮我解释一下逻辑吗?
  • 那么“期望的输出”是什么?
  • 应该不难。相信我。想想while

标签: java algorithm math dynamic-programming


【解决方案1】:

我认为 Tristan 是对的——你无法知道哪个操作最终会产生最短路径,所以你必须尝试所有这些才能得到正确的答案。

通常,像这样的蛮力解决方案意味着计算将花费指数级时间。您从 n 开始,然后使用您的 3 个操作计算最多 3 个新数字。然后对于这 3 个数字中的每一个,您会得到另外 3 个,总共 9。然后对于这 9 个中的每一个,您会得到另外 3 个,总共 27;等等。你可以看到你会如何很快地得到很多荒谬的可能性。

但是,您在此处的搜索空间有限。由于您的所有操作都会导致该值减小,因此您只会遇到从 1 到 n 的数字,包括 1 到 n。这意味着最多需要 n 次操作才能达到您的目标 1。每个数字只有一条最短路径,因此一旦找到该路径,您就不需要考虑任何您发现导致相同数字的其他路径。如果您保留一组以前看到的数字,您应该能够消除大部分搜索空间,因为您可以丢弃重复的结果。 (这是Memoization的一种形式。)

我会这样做:

  1. 创建一个Set<Integer> 以包含以前见过的值。
  2. 创建一个Map<Integer, Integer> 来保存您的active 值。每个 key → value 条目的键将是从 n1 的路径中的一个数字,而该值将是达到该数字所需的操作数。
  3. 将初始条目 n0 放入您的 active 地图中。
  4. 虽然您的 active 映射不包含值为 1(您的目标)的键:
    1. 创建一个空地图来保存您的新的活动值。
    2. 对于 active xi 中的每个条目:
      1. 如果 x 可以被 3 整除且 x/3 不在 seen 集合中,则添加 x/3 到 seen 并将 x/3 → i+1 放入您的新活动地图中。李>
      2. x/2 和 x-1 执行类似的操作。
    3. 将您当前的活动地图替换为新活动地图。
  5. 在您的活动地图中返回条目1的值ii

您可以做一些事情来加快速度(例如,当您找到1 时立即跳出循环),或减少内存占用(例如,如果他们从所见集合中丢弃哨兵'比您的任何活动条目都大,或者使用列表而不是地图,因为迭代的所有 i 值都是相同的),但这应该足以满足您的需要。


我已将我的解决方案移植到 Java 并在此处发布:

http://ideone.com/qWt0LE

输出包含一些时间。请注意,此处链接的解决方案使用 seen 的地图和 active 的列表。我将链中的前一个数字存储为 seen 中每个映射条目的值,这允许我在最后重建链。在输出中,3表示除以3,2表示除以2,1表示减去1

【讨论】:

  • “你无法知道哪个操作最终会产生最短路径,所以你必须尝试所有这些才能得到正确的答案。”这听起来不太可能。你有什么理由吗?
  • @NiklasB。 - 不,只是直觉。这让我想起了Collatz conjecture,也让我想起了很多其他的优化问题。除非我们可以证明,否则我们必须假设没有办法告诉一些 n 哪个操作将产生最短路径而不尝试所有 3 个选项。看看特里斯坦的回答。有没有办法告诉 n=10 或 n=11 最好的路径是什么而不只是尝试它?
  • @DaoWen:我最初的直觉是,你也许可以使用 DP 对小的 n(低于 10^6 左右)最优地解决问题,然后使用贪婪策略大于此的数字。原来我找不到这样的策略,但也许the guys over at math.SE比我知道的更多,这很有可能:)
  • @NiklasB。 - 我的解决方案使用记忆化,这是动态编程的基础。每次迭代都在活动列表中的 O(n) 个元素上工作,并且最多有 O(log n) 次迭代,这意味着总复杂度为 O (n log n)。我怀疑你能做得比这更好。不过,我很想看看 Math 网站上的人们怎么说!
  • 它实际上可能是 O(n) 因为每个函数调用你只做 O(1) 工作并且你只有 n 不同的参数集。但我想使用简单的 BFS 并修剪当前级别上超过最小值 3 倍的值将为您提供一个相当快速的算法。虽然不确定它是否是 O(log^cn),但很可能是。
【解决方案2】:

我认为这里的问题只是计算最少步骤数,而不是关于步骤的详细信息、如何处理它等。所以在这种情况下,下面的解决方案应该是有效且最简单的解决方案。动态编程中的自下而上方法。

  int getMinOperations(int n) {
    // Use this array to store the previous solved result.
    int dp[] = new int[n + 1];
    // base case, if it is 1, we do not need to do any processing
    dp[1] = 0;

    for (int i = 2; i <= n; i++) {
        // for the time being, let's assume we are getting minimum number of step by subtracting 1
        dp[i] = 1 + dp[i - 1];
        // if number if divisible by 2 let's divide it and compare it with the number of steps 
        // calculated in previous step, choose the minimum one
        if (i % 2 == 0)
            dp[i] = Math.min(dp[i], 1 + dp[i / 2]);
        // if number if divisible by 3 let's divide it and compare it with the number of steps 
        // calculated in previous step, choose the minimum one
        if (i % 3 == 0)
            dp[i] = Math.min(dp[i], 1 + dp[i / 3]);
        // At this step we have stored the minimum step to reduce the i to 1. and we will continue till nth value
    }
       // Returning nth value of array, as value at each index is the minimum number of steps to reduce it to 1.
    return dp[n];
}

【讨论】:

    【解决方案3】:

    最简单的解决方案可能是探索所有可能性。

    public static ArrayList<Integer> solve(int n, 
      ArrayList<Integer> moves, int bestMove,HashMap<Integer,Integer> memory) {
    
            if (moves.size() >= bestMove) return null;
            if (n == 1) return moves;
            Integer sizeOfPathN= memory.get(n);
    
            if (sizeOfPathN!=null && sizeOfPathN<=moves.size())return null;
            memory.put(n,moves.size());
    
            int size_1=Integer.MAX_VALUE, size_2 = Integer.MAX_VALUE, size_3 = Integer.MAX_VALUE;
            ArrayList<Integer> moves3 = null, moves2 = null, moves1;
    
            if (n % 3 == 0) {
                ArrayList<Integer> c = new ArrayList<Integer>(moves);
                c.add(3);
                moves3 = solve(n / 3, c,bestMove,memory);
                if (moves3!=null)
                size_3 = moves3.size();
            }
    
            bestMove = Math.min(bestMove, size_3);
    
            if (n % 2 == 0) {
                ArrayList<Integer> c = new ArrayList<Integer>(moves);
                c.add(2);
                moves2 = solve(n / 2, c,bestMove,memory);
                if (moves2!=null)
                size_2 = moves2.size();
            }
    
            bestMove = Math.min(bestMove, size_2);
    
    
            ArrayList<Integer> c = new ArrayList<Integer>(moves);
            c.add(1);
            moves1 = solve(n - 1, c,bestMove,memory);
            if (moves1!=null)
            size_1 = moves1.size();
    
            int r = Math.min(Math.min(size_1, size_2),size_3);
            if (r==size_1) return moves1;
            if (r==size_2) return moves2;
    
            return moves3;
    
        }
    

    解释

    n:n

    moves : 一个包含动作的 ArrayList。 (用于打印浇注)

    bestMove : 包含找到的最小解的大小的值。

    memory : 一个 HashMap,包含之前探索的“状态”和路径的长度。

    如果我们调用 公共静态 void main(String[] args) {

        long a = System.currentTimeMillis();
        Object[] sol=solve(10, new ArrayList<Integer>(),Integer.MAX_VALUE,new HashMap<Integer,Integer>()).toArray();
        System.out.println(sol.length);
        System.out.println(Arrays.toString(sol));
        System.out.println((System.currentTimeMillis()-a));
    }
    

    输出将是:

    3
    [1, 3, 3]
    1
    

    相当于n-1, n/3,n/3(@Tristan 的最佳解决方案)

    如果我们用1000 000 000 作为 n 来调用它:

    30
    [1, 3, 3, 3, 3, 1, 3, 3, 1, 3, 1, 1, 3, 3, 3, 3, 1, 2, 2, 1, 3, 2, 1, 3, 3, 2, 1, 3, 2, 2]
    55
    

    如果我们用11 调用它:

    4
    [1, 1, 3, 3]
    1
    

    编辑: 如果只需要移动次数:

    public static int solve(int n,int moves,int bestMove,HashMap<Integer,Integer> memory) {
    
            if (moves >= bestMove) return Integer.MAX_VALUE;
            if (n == 1) return moves;
            Integer sizeOfPathN= memory.get(n);
    
            if (sizeOfPathN!=null && sizeOfPathN<=moves)return Integer.MAX_VALUE;
            memory.put(n,moves);
    
            int size_1=Integer.MAX_VALUE;
            int size_2 = Integer.MAX_VALUE;
            int size_3 = Integer.MAX_VALUE;
    
            moves=moves+1;
            if (n % 3 == 0) size_3 = solve(n / 3, moves,bestMove,memory);
            bestMove = Math.min(bestMove, size_3);      
            if (n % 2 == 0) size_2=solve(n >> 1, moves,bestMove,memory);
    
            bestMove = Math.min(bestMove, size_2);
    
            size_1 = solve(n - 1, moves,bestMove,memory);
    
    
            return  Math.min(Math.min(size_1, size_2),size_3);
    
    
        }
    

    调用这个方法

    long a = System.currentTimeMillis();
    System.out.println(
         solve(1000 *1000*1000, 0,Integer.MAX_VALUE,new HashMap<Integer,Integer>()));
    
        System.out.println((System.currentTimeMillis()-a));
    

    输出:

    30
    24
    

    够快

    【讨论】:

    • 10^9 需要多长时间?
    • @BoristheSpider 在 i3 中 30 秒 :)
    • @BoristheSpider 对不起。 20.
    • @Tyler 你需要这个序列吗?它运行得更快,无需复制ArrayList
    • @rpax - 啊,我没有意识到你已经更新了你的答案来使用记忆。我只是在看 cmets 说用路径计算它需要 30 秒,然后你更新它,只计算长度需要 24 秒。我认为整个改进只是因为你删除了所有路径跟踪的东西。你的答案基本上和我现在的一样(你用递归做 DFS,而我用迭代做 BFS,但它们基本上是一样的),所以难怪它们的运行时间差不多。
    【解决方案4】:

    这是一个值得思考的有趣案例。 从 10 个中,您有几个选项:

    10 / 2  = 5     1 move
    5 - 1   = 4     2 moves
    4 / 2   = 2     3 moves
    2 - 1   = 1     4 moves
    
    
    10 - 1  = 9     1 move
    9 / 3   = 3     2 moves
    3 / 3   = 1     3 moves
    

    如果一个数是 2 不能被 3 整除呢? 从 11 开始,我们有以下选项:

    11 - 1  = 10    1 move
    10 / 2  = 5     2 moves
    5 - 1   = 4     3 moves
    4 / 2   = 2     4 moves
    2 / 2   = 1     5 moves
    
    
    11 - 1  = 10    1 move
    10 - 1  = 9     2 moves
    9 / 3   = 3     3 moves
    3 / 3   = 1     4 moves
    

    也许这只有在你减去的数字也能被 3 整除的情况下才有效?谁知道呢,祝你好运。

    【讨论】:

    • 没错。可能有很多情况。我需要逐一检查吗?
    • 我想你可能不得不:P。
    • @Tristan 看看我的回答。也许现在我是对的。
    【解决方案5】:

    注意:这不是实际代码,您需要考虑极端情况,这是一个伪代码,它给出了算法的逻辑

    这可以递归完成,但是由于n非常高,你需要使用额外的空间来记忆。

        fun(int n)
       {
            if(n==0) return INFINITY;
            if(n==1) return 0;  
    
            if(n%3==0)
            a = fun(n/3)
    
            if(n%2==0)
            b = fun(n/2)
    
            return min(fun(n-1) , a+1 , b+1)
    }
    

    【讨论】:

    • 这似乎是您必须这样做的方式(检查所有可能的路径并找到最小值)。但是,您没有初始化ab,所以这段代码不太正确。不过,它应该给 OP 一个正确的想法。
    • 提问者的真正问题是他们应该使用什么算法——显​​示代码(即使那是正确的)可能对他们最初的问题没有帮助。此外,出于空间考虑,这段代码(即使它是正确的)也不是一个好例子。
    • 这个程序会崩溃,n = 1e6
    【解决方案6】:

    使用动态编程的两种解决方案的 Python 实现

    1) 自上而下

    def calculate(mins, n):
        if n<= 0:
            raise ValueError("input is not positive")
        if n == 1:
            return 0
        if (mins[n] != -1):
            return mins[n]
    
        result = 1 + calculate(mins, n-1)
        if (n % 2 == 0):
            result = min(result, 1 + calculate(mins, n // 2))  
        if (n % 3 == 0):
            result = min(result, 1 + calculate(mins, n // 3))
    
        mins[n] = result
        return result
    
    def get_min_step_memoization (n):
        mins = [-1] * (n+1)
        return calculate(mins, n)
    

    2) 自下而上

    def get_min_step_dp(n):
        if n<= 0:
            raise ValueError("input is not positive")
        results = [-1] * (n+1)
        results[1] = 0 
    
        for idx in range(2, n+1):
            results[idx] = 1 + results[idx-1]
            if (idx % 2 == 0):
                results[idx] = min(results[idx], 1 + results[idx//2])
            if (idx %3 == 0):
                results[idx] = min(results[idx], 1 + results[idx//3])
    
        return results[n]
    

    【讨论】:

      【解决方案7】:

      使用记忆来提高速度。

      初始化你的地图:

      n_to_steps = {1=>0, 2=>1, 3=>1}
      

      现在计算。不要计算从 1 到 n 的所有内容……而是:

      def f(n)
        if n isn't a key in our map
          n_to_steps[n] = min(1 + n%2 + f(n/2), 1 + n%3 + f(n/3))
        end
        return n_to_steps[n]
      end
      

      【讨论】:

        【解决方案8】:

        我认为n= 10^9 指向一些逻辑,而不仅仅是尝试所有可能的情况和所有排列。
        这是我得到的:

        1 -> 0  
        2 -> 1  
        3 -> 1  
        4 -> 2  
        5 -> 3  
        6 -> 2  
        7 -> 3  
        8 -> 3  
        9 -> 2  
        10 ->3  
        11 ->4  
        12 ->3  
        13 ->4  
        14 ->4  
        15 ->3  
        16 ->4  
        17 ->5  
        18 ->3  
        19 ->4  
        20 ->4  
        21 ->4  
        

        所以我认为 fornula 是:

            while(n!=1)
            {
             if(n%3==0)
               ans += 1  and  n/=3 
              else if (n%2==0)
               ans += 1  and  n/=2
              else n--  , ans+=1
            }  
        

        在 0.06 秒内给出 N = 1000000000 的答案。 http://ideone.com/0pfKoz

        // 以前的错误逻辑:(@Dukeling 指出的 n=30 很容易失败)

        while(n!=1)
        {
         if(n%2==0 && n%3==0)
           ans += p1 + p2   and  n=1  {where n = 2^p1 * 3^p2 }
          else if (n%2!=0 && n%3 !=0)
           n--
          else if (n%2==0 && n%3 !=0)
           ans+=1  and n/=2 
          else if (n%2!=0 && n%3 ==0)
           ans+=1  and n/=3 
        }   
        

        请澄清你是否得到了这个问题的答案,并尝试看看我的逻辑是否有效。

        【讨论】:

        • 我相信 10 会给您的代码提供 4 而不是 3。
        • 这里发生了什么 - ans += n/3 + n/2。基于其他人,我认为ans 是计数,但这并没有使它看起来像它。你是怎么找到n/3 + n/2的?对我来说最接近的东西是n = n/3/2; ans++
        • 关于你的编辑 - 如果n = 30, n%2==0 &amp;&amp; n%3==0 为真,但n 不能写成2^p1 * 3^p2
        • @Dukeling 因为可能有很多这样的情况,我觉得如果不考虑所有情况就很难给出答案。
        • 感谢您的编辑。但是,当 n=11 时,我如何在 p1 和 p2 中表达它?
        猜你喜欢
        • 2022-08-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-17
        • 2013-05-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多