2026-08-03:统计网格路径中好整数的数目。用go语言,给定一个整数区间 [l, r],以及一个方向字符串 directions,这个字符串中恰好包含 3

24 阅读12分钟

2026-08-03:统计网格路径中好整数的数目。用go语言,给定一个整数区间 [l, r],以及一个方向字符串 directions,这个字符串中恰好包含 3 个字母 'D' 和 3 个字母 'R'。

对于区间里的每个整数 x,先将它补成 16 位数字:如果位数不足 16 位,就在左侧补 0。然后把这 16 个数字按行从左到右依次填入一个 4 × 4 的方格中,也就是前 4 个数字填第一行,接下来 4 个数字填第二行,依此类推。

接着,从方格左上角出发,按照 directions 中的顺序依次移动:遇到 'D' 就向下走一格,遇到 'R' 就向右走一格。过程中把经过的格子里的数字记录下来,起点也算在内,因此一共会得到 7 个数字。

如果这 7 个数字组成的序列是非递减的,就称 x 是一个好整数。最终需要统计并返回 [l, r] 内好整数的个数。

1 <= l <= r <= 9000000000000000。

directions.length == 6。

directions 由 恰好 三个 'D' 字符和三个 'R' 字符组成。

输入: l = 8, r = 10, directions = "DDDRRR"。

输出: 2。

解释:

x = 8 的网格:

0000
0000
0000
0008

路径:(0,0) → (1,0) → (2,0) → (3,0) → (3,1) → (3,2) → (3,3)

访问的数字序列为 [0, 0, 0, 0, 0, 0, 8]。

由于访问的数字序列是非递减的,因此 8 是一个好整数。

x = 9 的网格:

0000
0000
0000
0009

访问的数字序列为 [0, 0, 0, 0, 0, 0, 9]。

由于访问的数字序列是非递减的,因此 9 是一个好整数。

x = 10 的网格:

0000
0000
0000
0010

访问的数字序列为 [0, 0, 0, 0, 0, 1, 0]。

由于访问的数字序列不是非递减的,因此 10 不是一个好整数。

因此,只有 8 和 9 是好整数,在该范围内总共有 2 个好整数。

题目来自力扣3906。

步骤一:问题建模与前缀和转化

题目要求在区间 [l, r] 内统计“好整数”的个数。
一个好整数 x 的定义包含三个要素:

  • x 补足到 16 位(不足左侧补 0)。
  • 按行优先填入 4×4 网格,格子编号 015(左上角 0,右下角 15)。
  • 按照给定的方向字符串 directions(恰好 3 个 'D'、3 个 'R')从左上角出发,收集途经 7 个格子内的数字,若这个长度为 7 的数字序列非递减,则 x 为好整数。

为便于计数,算法转化为求前缀个数
F(N) 表示 [0, N] 中好整数的个数,则答案 = F(r) - F(l-1)
代码中实际实现了函数 solve(s),它返回小于数字串 s 的好整数个数。
因此 F(r) = solve(str(r+1))F(l-1) = solve(str(l))(这里 l 已经被当作下界传入,相当于 l 是原 l,减法由 solve(highS) - solve(lowS) 直接完成,其中 highS = r+1, lowS = l)。
这样做的好处是:可以用统一的上界比较逻辑处理所有情况,并且自然地包含前导零。


步骤二:确定数字的位数与路径映射

  • highS = str(r+1),令 n = len(highS)
    因为 r 最大为 9×10^15(16 位),r+1 可能为 10^16(17 位),故 n 为 16 或 17。
  • lowS = str(l) 左补 '0' 至长度 n,使两个字符串长度一致,方便数位 DP 对齐处理。
  • 原网格有 16 个格子,映射到线性下标 0…15。在 n 位数字串中,真正的 16 个格子位于最低的 16 位,即下标 n-16n-1。高位(如果有第 17 位)只能是 0
  • 根据 directions 的 6 步移动,模拟出在 4×4 网格中的路径:
    • 起点为左上角(线性下标 0)。
    • 每步 'R' 表示向右,下标 +1'D' 表示向下,下标 +4
    • 由于路径长度为 7(含起点),且只关心网格中的格子,路径下标只会落在 0…15 内。
  • 将这些路径下标映射到 n 位数字串的对应位上:
    网格下标 g 对应数字串下标 pos = (n - 16) + g
    用一个布尔数组 inPath 标记这 7 个位置,表示这些位上的数字必须构成非递减序列

步骤三:预处理组合数与后缀信息

为了快速计算非递减序列的方案数,首先预处理组合数 comb[i][j]i 最大约 n+10,这里 maxM=7,实际只需到 17 左右)。组合数用于计算“从若干数字中可重复地选取若干项且保持非递减”的组合数(即插板法)。

接着计算后缀数组 suf

  • suf[i] 表示在数字串下标 [i, n-1] 范围内,有多少位位于路径上(即 inPath 为真的位数)。
  • 这个值在后面会被用来快速知道“剩余未处理位中还有 m = suf[i+1] 个路径位”。

步骤四:实现数位 DP 函数 solve(s)

函数 solve(s) 统计所有 n 位数字串(含前导零)中字典序严格小于 s 且满足路径非递减约束的个数
遍历 i0n-1(高位到低位),维护变量 pre:表示路径上前一个已确定位的数字值(初始 pre = 0,因为序列非递减且数字为 0–9)。

对于当前位 i,设上限数字 hi = s[i] - '0',剩余路径位个数 m = suf[i+1]

情况 1:当前位 i 不在路径上

  • 这一位的数字没有任何单调性约束,可以独立选取。
  • 为了确保构成的数严格小于 s,我们让这一位取 0hi-1 中的任意值(共 hi 种),对于每种取值,后续位的填法分为两部分:
    1. 剩余 m 个路径位:它们必须形成一个以 pre 为下限的非递减序列。从数字 pre910 - pre 种数字,可重复地取 m 个并保持非递减。根据组合数学,方案数为 C(m + 9 - pre, m)
    2. 剩余的非路径位:共 (n-1-i) - m 位,每位可任意填 0–9,方案数为 10^{(n-1-i) - m}
  • 两者相乘再乘以 hi,累加到结果中。
  • 随后,隐式地将当前位固定为 hi(即等于上限),不做额外操作,直接进入下一位循环(通过 continue 实现),因为此时仍需继续匹配上界。

情况 2:当前位 i 在路径上

  • 路径序列要求非递减,因此当前位可选的数字 d 必须满足 pre ≤ d < hi
  • hi < pre,则连最小的合法值 pre 都超过了上限,无法填任何合法数字,直接终止循环。
  • 否则 hi ≥ pre,对每一个合法的 d (prehi-1),剩余位的方案数为:
    • 路径位:从 d9 中可重复取 m 个非递减,方案数为 C(m + 9 - d, m)
    • 非路径位:仍然为 10^{(n-1-i) - m}
  • dprehi-1 的方案数求和。利用组合恒等式,该和可化简为:
    (C(m + 10 - pre, m + 1) - C(m + 10 - hi, m + 1))
  • 将求和结果乘以 10^{(n-1-i) - m} 并累加。
  • 处理完所有小于 hi 的分支后,将 pre 更新为 hi,表示当前位取 hi 以继续匹配上界,进入下一位。

遍历结束后,函数返回累加的结果 res,这就是严格小于 s 的好整数个数


步骤五:计算最终答案

  • 调用 solve(highS) 得到 [0, r] 的好整数个数(因为 highS = r+1,统计小于 r+1 即是 ≤ r)。
  • 调用 solve(lowS) 得到 [0, l-1] 的好整数个数(lowS = l,统计小于 l 即是 ≤ l-1)。
  • 两者相减即为区间 [l, r] 内的好整数个数。

复杂度分析

  • 时间复杂度
    组合数预处理为常数时间(规模与 maxM 相关,不超过 18×8)。
    countGoodIntegersOnPath 中,字符串转换、路径标记、后缀数组计算均是 O(n),其中 nr+1 的十进制位数,最大为 17。
    solve 函数遍历 n 位,每次迭代仅进行常数次组合数查表、幂运算和算术操作,因此 solve 也是 O(n)
    总体时间复杂度为 O(n),由于 n ≤ 17,实际上可以视为 O(1),与区间大小无关。

  • 额外空间复杂度
    组合数表格占用常数空间。
    字符串、inPathsuf 等数组长度均为 O(n),常数上界很小。
    递归或栈空间为 O(1)
    因此总额外空间复杂度为 O(n),实际也是 O(1)

Go完整代码如下:

package main

import (
	"fmt"
	"math"
	"strconv"
	"strings"
)

const maxM = 7

var comb [maxM + 10][maxM + 1]int

func init() {
	// 预处理组合数
	for i := range comb {
		comb[i][0] = 1
		for j := 1; j < min(i+1, len(comb[i])); j++ {
			comb[i][j] = comb[i-1][j-1] + comb[i-1][j]
		}
	}
}

func countGoodIntegersOnPath(l, r int64, directions string) int64 {
	highS := strconv.FormatInt(r+1, 10) // 注意这里加一了
	n := len(highS)
	lowS := strconv.FormatInt(l, 10)
	lowS = strings.Repeat("0", n-len(lowS)) + lowS

	inPath := make([]bool, n)
	pos := n - 16 // 右下角是下标 n-1,那么左上角是下标 n-16
	for _, d := range directions {
		if pos >= 0 { // 只需要对网格图中的后 n 个格子做标记
			inPath[pos] = true // 标记在路径中的格子
		}
		if d == 'R' { // 往右
			pos++
		} else { // 往下
			pos += 4 // 相当于往右数 4 个位置
		}
	}
	inPath[n-1] = true // 终点一定在路径中

	// suf[i] 表示后缀 [i, n-1] 在路径中的下标个数
	suf := make([]int, n+1)
	for i := n - 1; i >= 0; i-- {
		suf[i] = suf[i+1]
		if inPath[i] {
			suf[i]++
		}
	}

	// 计算小于 r 的合法整数个数
	solve := func(r string) (res int) {
		pre := 0
		for i, ch := range r {
			hi := int(ch - '0')
			m := suf[i+1]
			if !inPath[i] {
				res += hi * comb[m+9-pre][m] * int(math.Pow10(n-1-i-m))
				continue
			}
			if hi < pre {
				break
			}
			res += (comb[m+10-pre][m+1] - comb[m+10-hi][m+1]) * int(math.Pow10(n-1-i-m))
			pre = hi // 这一位填 hi,继续计算剩余数位的方案数
		}
		return res
	}

	return int64(solve(highS) - solve(lowS))
}

func main() {
	l := 8
	r := 10
	directions := "DDDRRR"
	result := countGoodIntegersOnPath(int64(l), int64(r), directions)
	fmt.Println(result)
}

在这里插入图片描述

Python完整代码如下:

# -*-coding:utf-8-*-

import math

def count_good_integers_on_path(l: int, r: int, directions: str) -> int:
    # 将上界加一,方便计算小于等于r的个数
    high_s = str(r + 1)
    n = len(high_s)
    # 将下界补零到相同长度(用于数位DP)
    low_s = str(l).zfill(n)

    # 标记路径上的格子(对应16位数字的最后16位)
    in_path = [False] * n
    pos = n - 16          # 起始位置(对应16位网格的左上角)
    for d in directions:
        if pos >= 0:
            in_path[pos] = True
        if d == 'R':
            pos += 1
        else:             # 'D'
            pos += 4
    in_path[n - 1] = True   # 终点一定在路径上

    # suf[i] 表示后缀 [i, n-1] 中路径格子的个数
    suf = [0] * (n + 1)
    for i in range(n - 1, -1, -1):
        suf[i] = suf[i + 1] + (1 if in_path[i] else 0)

    # 计算严格小于 s 的合法数字个数
    def solve(s: str) -> int:
        res = 0
        pre = 0  # 上一个路径格子的数字
        for i, ch in enumerate(s):
            hi = int(ch)
            m = suf[i + 1]   # 当前位置之后(不包括i)路径格子数
            if not in_path[i]:
                # 当前位置不在路径上,可以自由选择0..hi-1
                if hi > 0:
                    ways = math.comb(m + 9 - pre, m)
                    res += hi * ways * (10 ** (n - 1 - i - m))
                # pre 保持不变,因为该位置不影响路径序列
                continue
            else:
                # 当前位置在路径上,必须保证 >= pre
                if hi < pre:
                    break
                # 当前位可取 pre .. hi-1 的所有情况
                total = math.comb(m + 10 - pre, m + 1)
                ge = math.comb(m + 10 - hi, m + 1)   # 当前位 >= hi 的方案数
                res += (total - ge) * (10 ** (n - 1 - i - m))
                pre = hi   # 更新上一个路径数字为当前选择
        return res

    # 区间计数 = (小于 r+1 的个数) - (小于 l 的个数)
    return solve(high_s) - solve(low_s)


if __name__ == "__main__":
    l, r = 8, 10
    directions = "DDDRRR"
    result = count_good_integers_on_path(l, r, directions)
    print(result)

在这里插入图片描述

C++完整代码如下:

#include <iostream>
#include <string>
#include <cmath>
#include <algorithm>
#include <vector>

using namespace std;

const int MAX_M = 7;
long long comb[MAX_M + 10][MAX_M + 1];

// 预处理组合数
void initComb() {
    for (int i = 0; i < MAX_M + 10; i++) {
        comb[i][0] = 1;
        for (int j = 1; j < min(i + 1, MAX_M + 1); j++) {
            comb[i][j] = comb[i-1][j-1] + comb[i-1][j];
        }
    }
}

// 计算小于 r 的合法整数个数(这里的r是字符串形式)
int solve(const string& r, const vector<bool>& inPath, const vector<int>& suf, int n) {
    int res = 0;
    int pre = 0;  // 上一个路径格子的数字

    for (int i = 0; i < n; i++) {
        int hi = r[i] - '0';
        int m = suf[i + 1];  // 当前位置之后路径格子的个数

        if (!inPath[i]) {
            // 当前位置不在路径上,可以自由选择
            res += hi * comb[m + 9 - pre][m] * (int)pow(10, n - 1 - i - m);
            continue;
        }

        // 当前位置在路径上
        if (hi < pre) {
            break;  // 无法满足非递减条件
        }

        // 当前位可取 pre..hi-1 的所有情况
        res += (comb[m + 10 - pre][m + 1] - comb[m + 10 - hi][m + 1]) * (int)pow(10, n - 1 - i - m);
        pre = hi;  // 更新上一个路径数字为当前选择
    }

    return res;
}

long long countGoodIntegersOnPath(long long l, long long r, string directions) {
    // 将上界加一,方便计算小于等于r的个数
    string highS = to_string(r + 1);
    int n = highS.length();

    // 将下界补零到相同长度
    string lowS = to_string(l);
    lowS = string(n - lowS.length(), '0') + lowS;

    // 标记路径上的格子(对应16位数字的最后16位)
    vector<bool> inPath(n, false);
    int pos = n - 16;  // 起始位置(对应16位网格的左上角)

    for (char d : directions) {
        if (pos >= 0) {
            inPath[pos] = true;  // 标记在路径中的格子
        }
        if (d == 'R') {
            pos++;  // 向右
        } else {  // 'D'
            pos += 4;  // 向下,相当于向右移动4个位置
        }
    }
    inPath[n - 1] = true;  // 终点一定在路径中

    // suf[i] 表示后缀 [i, n-1] 中路径格子的个数
    vector<int> suf(n + 1, 0);
    for (int i = n - 1; i >= 0; i--) {
        suf[i] = suf[i + 1];
        if (inPath[i]) {
            suf[i]++;
        }
    }

    // 区间计数 = (小于 r+1 的个数) - (小于 l 的个数)
    int result = solve(highS, inPath, suf, n) - solve(lowS, inPath, suf, n);
    return (long long)result;
}

int main() {
    // 预处理组合数
    initComb();

    // 测试用例
    long long l = 8;
    long long r = 10;
    string directions = "DDDRRR";
    long long result = countGoodIntegersOnPath(l, r, directions);
    cout << result << endl;

    return 0;
}

在这里插入图片描述