寻找最少的移动步数

8
我有以下问题陈述:
给定一个数字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);

但我没有得到期望的输出。有人能帮我吗?


2
问题不在于代码,而在于逻辑。修正你的逻辑。 - devnull
添加了n的范围。 - Abhiroop Sarkar
2
那么“期望输出”是什么? - Brett Hale
不应该很难。相信我。想想“while”。 - devnull
很希望能看到这方面的理论分析。我相信一定有比“愚蠢”的BFS或动态规划更好的算法。不幸的是,这里的答案并没有真正触及要点。 - Niklas B.
显示剩余10条评论
8个回答

7
我认为Tristan是正确的-你没有办法知道哪个操作最终会产生最短的路径,所以必须尝试所有操作才能得到正确答案。
通常,像这样的暴力解决方案意味着计算将花费指数时间。您从n开始,然后使用3个操作计算出3个新数字。然后对于这3个数字中的每一个,您都会获得另外3个数字,共计9个数字。然后对于这9个数字中的每一个,您都会获得另外3个数字,总计27个数字;以此类推。您可以看到,您很快就会有一个荒谬的可能性数量。
然而,您的搜索空间受限。由于所有操作都会使值“减少”,因此您只会遇到从1到n(含)的数字。这意味着最多需要n次操作才能达到目标1。每个数字只有一条最短路径,因此一旦找到该路径,您就不需要考虑任何其他导致相同数字的路径。如果保留先前看到的数字集合,您应该能够消除大部分搜索空间,因为可以丢弃重复结果。(这是记忆化的一种形式。)
以下是我解决该问题的方法:
  1. 创建一个包含先前“已见”值的Set<Integer>
  2. 创建一个Map<Integer,Integer>来保存您的“活动”值。每个键→值条目的键将是从n1的路径中的数字,而值将是到达该数字所需的操作数。
  3. 在您的active映射中放入初始条目n0
  4. 当您的active映射不包含值为1的键(即目标)时:
    1. 创建一个空映射以保存您的新活动值。
    2. 对于active中的每个条目xi
      1. 如果x可被3整除且x/3不在seen集合中,则将x/3添加到seen中,并将x/3 → i+1放入您的new active映射中。
      2. 对于x/2和x-1做类似的操作。
    3. new active映射替换当前的active映射。
  5. 在您的active map中返回条目1i的值。

有几件事情可以让这个过程更快一些(例如,在找到1时立即跳出循环),或者减少内存占用(例如,如果哨兵比任何一个活动条目都大,则将其丢弃,或者使用列表而不是映射,因为迭代的所有i值都相同),但这应该足够高效地完成您需要的工作。


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

http://ideone.com/qWt0LE

输出包含一些时间信息。请注意,此处链接的解决方案使用了一个映射(map)来存储“seen”,并使用一个列表(list)来存储“active”。我将链中的前一个数字存储为“seen”中每个映射条目的值,这使我能够在最后重构链。在输出中,3表示“除以3”,2表示“除以2”,1表示“减1”。

1
“你无法知道哪个操作最终会产生最短的路径,因此必须尝试所有操作才能得到正确的答案。”听起来很不可思议。你有任何理由支持这个说法吗? - Niklas B.
@NiklasB. - 不是的,只是凭直觉。它让我想起了Collatz猜想,也让我想起了其他几个优化问题。除非我们能证明否定,否则我们必须假设对于某些n,没有办法在尝试所有3个选项之前确定哪个操作会产生最短的路径。只看Tristan的答案。有没有办法在n=10或n=11时确定最佳路径而不仅仅是尝试? - DaoWen
@DaoWen:我的最初直觉是,你可以使用DP来解决小规模的问题(小于10^6),然后对于大于这个范围的数字采用贪心策略。结果我发现我找不到这样的策略,但也许在math.SE上的那些人比我知道得多,这很可能 :) - Niklas B.
@NiklasB. - 我的解决方案使用记忆化技术,这是动态规划的基础。每次迭代在活动列表中跨越*O(n)个元素,最多有O(log n)次迭代,这意味着总复杂度为O(n log n)*。我怀疑你不可能做得比这更好。不过,我很想看看数学网站上的人们会说些什么! - DaoWen
它实际上可能是O(n),因为每个函数调用只需要O(1)的工作量,而且你只有n个不同的参数集。但我猜使用简单的BFS并修剪当前级别上超过最小值3倍的值将得到一个相当快速的算法。虽然不确定它是否是O(log^c n),但很可能是。 - Niklas B.
显示剩余2条评论

5

我认为这里的问题只是要计算最小步数,而不是关于步骤、如何处理等细节。因此,在这种情况下,下面的解决方案应该是高效且最简单的。使用动态规划的自底向上方法。

  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];
}

2

这是一个值得思考的有趣案例。 从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

如果从一个比3的倍数少2的数字开始呢? 从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整除时,这个方法才有效?谁知道呢,祝楼主好运。

是的,确切地说。可能会有很多情况。我需要逐一检查它们吗? - Abhiroop Sarkar
我觉得你可能得:P。 - Tristan
@Tristan 请看一下我的回答。也许现在我是正确的。 - rpax

2
最简单的解决方案可能是探索所有可能性。
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。

如果我们调用 public static 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

足够快


@BoristheSpider 在 i3 上只需要 30 秒 :) - rpax
@BoristheSpider 抱歉。20。 - rpax
现在我只需要序列的大小,即移动次数。你能优化代码以反映这一点吗? - Abhiroop Sarkar
我认为你没有考虑到代码的大O复杂度。@tyler可能正在尝试解决一个在线评测问题,其时间限制在你的电脑上约为0.2秒。 - Aseem Goyal
1
@rpax - 啊,我没意识到你更新了答案并使用了记忆化。我只是看到评论说用路径计算需要30秒,然后你更新了它,仅计算长度就只需要24秒。我以为整个改进都是因为你删除了所有的路径跟踪内容。现在你的答案基本上等同于我的(你使用递归进行DFS,而我使用迭代进行BFS,但它们基本上是相同的东西),所以它们有大约相同的运行时间也就不足为奇了。 - DaoWen
显示剩余15条评论

0

使用动态规划的两种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]

0

使用记忆化来提高速度。

初始化你的映射:

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

0

注意:这不是实际的代码,你需要考虑边角情况,这是一个伪代码,它提供了算法的逻辑

这可以通过递归来完成,但由于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提供正确的想法。 - DaoWen
提问者真正的问题是他们应该使用什么算法 - 显示代码(即使正确)可能无法帮助他们解决原始问题。此外,由于空间限制,即使这段代码是正确的,它也不是一个好的示例。 - ErstwhileIII
这个程序将在偶数时崩溃,n = 1e6。 - Fallen

-1

我认为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  

所以我认为公式是:

    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 
}   

请明确是否已经得到了这个问题的答案,并尝试看看我的逻辑是否正确。

这里发生了什么 - ans += n/3 + n/2。根据其他的代码,我认为ans是计数器,但这个式子好像不是这样的。你是怎么得到n/3 + n/2的?对我来说最有意义的东西是n = n/3/2; ans++ - Bernhard Barker
关于您的编辑 - 如果 n = 30,则 n%2 == 0 && n%3 == 0 为真,但 n 不能写成 2 ^ p1 * 3 ^ p2 - Bernhard Barker
@TylerпјҡжҲ‘иҜҙзҡ„жҳҜеҸӘжңүеҪ“nеҗҢж—¶иў«2е’Ң3ж•ҙйҷӨж—¶пјҢ11жүҚдёҚж»Ўи¶іиҝҷдёӘжқЎд»¶гҖӮ - Aseem Goyal
@Tyler:忘记写 ans+=1 了,在 n-- 的时候加上它。 - Aseem Goyal
它仍然返回4而不是3,当n = 10。 - Bernhard Barker
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接