题目描述

:::fold-green 相关原题

✅ 圆环回原点

牛客原题要求对 10^9 + 7 取模,本文保留不取模版本。

:::

圆环上有 10 个点,依次编号为 0 到 9,其中 0 和 9 也相邻。

给定非负整数 n,从点 0 出发,每一步可以顺时针或逆时针移动到相邻点。请返回恰好移动 n 步后回到点 0的不同走法数量。只要某一步的移动方向不同,就视为不同走法。

示例 1:

输入:n = 2
输出:2
解释:两种走法为 0 → 1 → 0 和 0 → 9 → 0。

提示:

  • 每一步必须移动,不能停留在当前点。
  • 可以多次经过同一个点,但必须走满 n 步。
  • n = 0 时,不移动算一种走法。
  • 答案不需要取模;下方整数接口适用于计数在可表示范围内的输入。

题意分析

圆环上有 10 个位置,依次编号为 0 到 9,相邻位置可以双向移动,首尾位置也相邻。从 0 出发,每次必须向左或向右移动一步,求恰好走 n 步后回到 0 的不同走法数量。

走法按每一步的方向序列区分,不允许停在原地。中途经过原点仍要继续走满规定步数;绕完整圈回到原点也算,因此不要求向左和向右的次数一定相等。零步不动算一种完成方式。

解法:圆环位置动态规划

核心思路

[!blue]

未来能怎样移动,只取决于已经走了多少步和当前停在哪里,不需要保存整条历史方向序列。令 ways[step][pos] 表示恰好走 step 步后停在 pos 的走法数。目标就是 ways[n][0],初始只有 ways[0][0] = 1,其他位置为零。

走完当前一步后要停在 pos,上一步只能位于它的两个邻点之一。左邻点的每条历史路径向右走一步,右邻点的每条历史路径向左走一步,都会到达当前点;两类的最后一步方向不同,互不重复且覆盖全部来源,因此把两个邻点上一轮的方案数相加即可。

圆环首尾相接,左邻点下标写成 (pos - 1 + 10) % 10,右邻点写成 (pos + 1) % 10。左边先加环长,是为了在 pos = 0 时也得到非负的合法下标,而不是依赖负数余数去访问数组。

每轮只依赖上一轮,可以把步数维度压缩掉:dp 保存已经走完的步数状态,另建 next 计算再走一步后的全部位置。必须把 next 的十个位置都计算完,再整体令 dp = next;若直接写回 dp,相邻位置可能读到本轮新值,混淆不同步数。

本题没有给出模数,代码保留精确加法,不擅自对答案取模。当前接口使用固定宽度整数,适用范围是所有中间计数都能由该整数类型表示的输入;若步数范围会超出这一前提,需要按实际要求选择大整数或题目指定的模数。

解题步骤

  1. 创建长度为 10 的零数组 dp,令 dp[0] = 1,表示零步时位于原点的唯一方案。
  2. 对每一步新建 next,依次枚举圆环的十个位置。
  3. 找到当前位置的左右邻点,把它们在旧 dp 中的方案数相加,写入 next[pos]。
  4. 本轮全部位置计算结束后,用 next 替换 dp,进入下一步。
  5. 完成恰好 n 轮后返回 dp[0],而不是累计中途经过原点的次数。

代码实现

class Solution {
    private static final int SIZE = 10;

    public int backToOrigin(int n) {
        int[] dp = new int[SIZE];

        // 零步留在原点也是一种方案,是所有转移的起点。
        dp[0] = 1;

        for (int step = 1; step <= n; step++) {
            // 本轮只读上一轮数组,不能原地覆盖邻点的旧状态。
            int[] next = new int[SIZE];

            for (int pos = 0; pos < SIZE; pos++) {
                int left = (pos - 1 + SIZE) % SIZE;
                int right = (pos + 1) % SIZE;

                next[pos] = dp[left] + dp[right];
            }

            dp = next;
        }

        return dp[0];
    }
}
func backToOrigin(n int) int {
    const size int = 10
    dp := make([]int, size)
    // 零步留在原点也是一种方案,是所有转移的起点。
    dp[0] = 1

    for step := 1; step <= n; step++ {
        // 本轮只读上一轮数组,不能原地覆盖邻点的旧状态。
        next := make([]int, size)
        for pos := 0; pos < size; pos++ {
            left := (pos - 1 + size) % size
            right := (pos + 1) % size
            next[pos] = dp[left] + dp[right]
        }
        dp = next
    }
    return dp[0]
}

复杂度分析

  • 时间复杂度:$O(n)$,每一步固定计算十个位置;若环长作为输入记为 m,则为 $O(nm)$。
  • 空间复杂度:$O(1)$,同时使用的两轮数组长度都固定为十;环长为 m 时为 $O(m)$。

关键点总结

[!green]

  • 状态同时包含确切步数和停留位置,才能区分中途返回与最终返回。
  • 按最后一步来自哪个邻点分类,得到没有重复或遗漏的计数转移。
  • 先完整计算新一轮,再替换旧一轮,避免相邻依赖被原地覆盖。
  • 偶数长度圆环每走一步都会改变位置编号的奇偶性,所以奇数步不可能回到编号为零的原点。

易错点总结

[!yellow]

  • 左邻点直接用负数取模,Java 和 Go 都可能得到负下标;应先加上环长。
  • 原地覆盖旧状态,会让一轮转移读取不同步数的数据。
  • 把 dp[0] 初始化成零,所有后续状态都会保持为零,无法生成任何路径。
  • 最后返回 dp[n],混淆了步数和位置;滚动后下标只表示圆环位置。
  • 只统计左右步数相同的方案,会遗漏方向净位移为整圈倍数的回原点路径。
  • 方案数随步数增长,固定宽度整数必须满足中间计数不溢出的适用前提;没有指定模数时不能随意取模改变答案。

相似题目

题目 难度 关联与区别
LCP 07. 传递信息 简单 恰好k轮有向图路径计数可复用,本题图是10个点的双向环且终点回到起点。
1269. 停在原地的方案数 困难 原题在线性有界数组上可原地停留,本题只能左右移动且首尾相接,邻居和边界规则不同。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/89102112
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!