[NOIP 2002 普及组] 产生数

GitHub跳转原题关系图返回列表

对十个数字求变换传递闭包,再把每一位的可达数字数相乘。

OJ: luogu

题目 ID: P1037

难度:普及-

标签:传递闭包乘法原理位运算pythoncpp

创建: 2026-07-17 03:00

更新: 2026-10-18 19:47

RBook 文章:
Floyd 全源最短路

题意

每一位数字可按规则变换任意次,求整个整数能产生多少种不同结果。

思路

十个数字上做传递闭包,reachable[d] 得到数字 d 最终可变成的集合。不同数位独立,根据乘法原理把每一位集合大小相乘。

Python 知识

  • 每个数字集合只需一个整数位掩码。
  • bit_count() 直接求可达数字种数。
  • Python 大整数能直接保存最多 30 位数字产生的巨大答案。

代码

位掩码 + 传递闭包(Python 主解)

把 reachable[d] 压成一个 10 位掩码,用 Floyd 的三重循环求闭包:中点必须是最外层循环,start 能到 middle 时把 middle 的掩码整个并入。

python
import sys


input = sys.stdin.buffer.readline
number, rule_count = input().split()

# 二进制数表示各个数字之间的转换的状态
reachable = [1 << digit for digit in range(10)]

for _ in range(int(rule_count)):
    source, target = map(int, input().split())
    reachable[source] |= 1 << target # 走一步

# 传递闭包 ,中点
for middle in range(10):

    # 把点转成 状态
    bit = 1 << middle
    # 枚举起点
    for start in range(10):
        # 查看起点是否可以到达中点
        if reachable[start] & bit:
            # 如果中点到终点可达,那么起点到终点可达
            reachable[start] |= reachable[middle]
answer = 1
for digit in number:
    # bit_count() 求该位数字可达的种数
    answer *= reachable[digit - 48].bit_count()

print(answer)

位掩码 + 传递闭包(C++)

思路与 Python 主解完全一致,只是用 long long 的低 10 位当掩码,__builtin_popcountll 数可达数字种数。答案最大约 103010^{30},超出 long long,所以用 __int128 保存并手写输出。

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-17 01:40
 * update_at: 2026-10-18 19:44
 */
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

string number;    // 原整数 n,最多 30 位,按字符串读
ll rule_count;    // 变换规则条数

// reachable[d] 是位掩码:第 b 位为 1 表示数字 d 能变成数字 b
ll reachable[10];

// 答案最大约 10^30,超过 long long,用 __int128 保存并自己输出
void print_i128(__int128 value) {
    if (value == 0) {
        cout << 0 << '\n';
        return;
    }

    string digits;
    while (value > 0) {
        char ch = '0' + value % 10;
        digits.push_back(ch);
        value /= 10;
    }
    reverse(digits.begin(), digits.end());
    cout << digits << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> number >> rule_count;

    // 每个数字至少能变成自己
    for (int digit = 0; digit < 10; digit++) {
        reachable[digit] = 1LL << digit;
    }

    for (ll i = 0; i < rule_count; i++) {
        ll source, target;
        cin >> source >> target;
        reachable[source] |= 1LL << target; // 走一步
    }

    // Floyd 传递闭包:中转数字 middle 必须在最外层
    for (int middle = 0; middle < 10; middle++) {
        ll middle_bit = 1LL << middle;
        for (int start = 0; start < 10; start++) {
            // start 能变成 middle,就把 middle 能变成的数字一并并入
            if (reachable[start] & middle_bit) {
                reachable[start] |= reachable[middle];
            }
        }
    }

    // 各数位互不影响,按乘法原理把每位可取的数字种数相乘
    __int128 answer = 1;
    for (char ch : number) {
        int digit = ch - '0';
        int kinds = __builtin_popcountll(reachable[digit]);
        answer *= kinds;
    }

    print_i128(answer);
    return 0;
}

集合 + DFS(Python 替代写法)

也可以不做闭包,直接对每个数字从它自己出发 DFS:nSet[d] 收集 d 在图上能走到的全部数字。dfs 靠集合做访问标记,因此环不会无限递归。这个写法更直白地表达“任意次变换”,代价是要真的存 10 个集合。

python
import sys

type Graph = list[list[int]]

# dfs 遍历 u 能到达的点
def dfs(g:Graph,u:int ,s:set):
    if u in s:
        return
    s.add(u)
    for v in g[u]:
        dfs(g,v,s)

def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    n = next(data)  # 0 <= n < 31,单个整数
    k = next(data)

    # build graph
    g:Graph = [[] for _ in range(10+1)]
    for _ in range(k):
        u = next(data)  # x >= 0
        v = next(data)  # y < n
        g[u].append(v)


    nSet:list[set] = [set() for _ in range(10)]
    # nSet 记录每个数字可以到达的数字的集合
    for i in range(10):
        dfs(g,i,nSet[i])
    ans = 1

    for c in str(n):
        # 有 len 种可能的变化
        ans *= len(nSet[int(c)])
    print(ans)




if __name__ == "__main__":
    solve()

复杂度

闭包是常数规模,扫描数字串 O(len(n));DFS 写法的图只有 10 个点和至多 15 条边,O(10 + k) 预处理后可 O(len(n)) 回答。

总结

每位独立选择时,先求单个符号的闭包,再用乘法原理组合。