欢迎光临
我们一直在努力

UVa 10782 Send More Money

题目描述

这是一道经典的 字母算术谜题(Cryptarithmetic Puzzle\\texttt{Cryptarithmetic Puzzle}Cryptarithmetic Puzzle) 题目。题目背景是一个小故事:哥哥为了拖延时间读完一本有趣的书,给弟弟出了一道谜题。这道谜题的形式是,将一个加法算式中的每个数字都用一个大写字母替换,相同的数字用相同的字母替换,不同的数字用不同的字母替换,并且数字不能有前导零。

任务就是给定这样一个被字母替换后的算式(可能不止两个加数),找出每个字母对应的原始数字,使得算式成立。

输入输出格式

输入格式

  • 第一行一个整数 TTTT≤10T \\le 10T10 ),表示测试用例的数量。
  • 对于每个测试用例:
    • 第一行一个整数 nnn2<n<62 < n < 62<n<6 ),表示算式中的“单词”数量。
    • 第二行包含 nnn 个由空格分隔的单词,每个单词由 111101010 个大写字母组成。最后一个单词是加法的和,其余 n−1n-1n1 个单词都是加数。

输出格式

  • 对于每个测试用例,输出一行,包含所有出现过的字母及其对应的数字。
  • 输出格式为 字母=数字,按字母的字典序(A 到 Z)排列,不同字母之间用一个空格分隔。
  • 题目保证每个测试用例有且仅有一个解。

约束条件

  • 一一映射:每个字母唯一对应一个 0−90-909 的数字,每个数字最多被一个字母使用。
  • 无前导零:每个“单词”(即数字)的首位字母不能对应数字 000 ,除非这个单词只有一位。
  • 唯一解:题目保证对于给定的输入,存在且只存在一种数字分配方案使得算式成立。

题目分析与解题思路

这道题的本质是一个约束满足问题(CSP\\texttt{CSP}CSP)。我们需要为有限个变量(字母)在有限的定义域(0−90-909的数字)内赋值,同时满足一系列约束:

  • 所有变量赋值互不相同。
  • 某些变量(作为单词首字母时)的值不能为 000
  • 所有赋值必须使得给定的算术等式成立。
  • 核心思路:回溯搜索(DFS\\texttt{DFS}DFS

    最直接的解决方法是回溯算法(Backtracking\\texttt{Backtracking}Backtracking),也称为深度优先搜索(DFS\\texttt{DFS}DFS)。

  • 状态定义:我们的状态是已经为一部分字母分配了数字。目标是给所有出现的字母都分配一个数字。
  • 搜索过程:
    • 我们按一定顺序(例如字母出现的顺序)依次处理每个还未被赋值的字母。
    • 对于当前字母,我们尝试所有未被使用的数字(000999)进行赋值。
    • 赋值后,进入下一层递归,处理下一个字母。
    • 如果为所有字母都成功赋值,则进入“验证”阶段。
    • 如果验证失败,或者在后续的搜索中发现无解,则回溯:撤销当前字母的赋值,尝试下一个数字。
  • 剪枝优化:为了尽早发现无效的赋值,减少搜索量,我们在两个地方进行判断(剪枝):
    • 赋值过程中:当为某个单词的首字母赋值时,如果赋值为 000 且该单词长度大于 111,则直接跳过此赋值(违反“无前导零”规则)。
    • 赋值完成后:在所有字母都被赋值后,我们计算所有加数对应的数值之和,再计算和单词对应的数值,判断两者是否相等。这是最终的约束检查。
  • 算法步骤详解

  • 数据读取与预处理:

    • 读取所有单词,存储在 vector<string> words 中。
    • 遍历所有单词的所有字符,用一个 set<char> 收集所有出现过的唯一字母,然后转存到 vector<char> letters 中,方便按索引处理。
    • 初始化两个全局数组:
      • int charToDigit[26]:将字母 'A' 到 'Z' 映射到数字,初始值为 -1 表示未赋值。
      • bool digitUsed[10]:标记数字 000999 是否已被使用。
  • 回溯函数 backtrack(int index):

    • 参数:index 表示当前即将处理 letters 中的第几个字母。
    • 基准情况:如果 index == letters.size(),说明所有字母都已赋值,执行最终验证。
      • 验证 111:检查所有单词首字母是否非零(除非单词长度为 111)。
      • 验证 222:计算加数之和 sum 与和单词之值 result,判断 sum == result。
      • 若验证通过,返回 true;否则返回 false。
    • 递归情况:取出当前字母 currentChar。
      • 遍历数字 000999
      • 如果该数字未被使用,则尝试赋值。
      • 重要剪枝:如果当前字母是某个单词的首字母,且该单词长度大于 111,而尝试赋值的数字是 000,则跳过此次尝试(违反规则)。
      • 标记数字已用,记录字母到数字的映射。
      • 递归调用 backtrack(index + 1)。
      • 如果递归返回 true,说明已找到解,直接返回 true。
      • 否则,进行回溯:清除数字使用标记,清除字母映射。
  • 输出结果:

    • 回溯成功后,charToDigit 数组中就存储了正确的映射。
    • 将所有 (字母, 数字) 对存入一个向量,按字母排序后输出即可。
  • 复杂度分析

    • 时间复杂度:最坏情况下需要尝试所有字母到数字的排列。设字母个数为 mmmm≤10m \\le 10m10),则最坏时间复杂度为 O(m!)O(m!)O(m!)O(10!)O(10!)O(10!)。由于约束较强,实际搜索空间远小于此,并且 TTT 很小,因此算法可以在时间内通过。
    • 空间复杂度:主要是递归栈的深度,为 O(m)O(m)O(m),以及存储单词和映射的辅助空间,总体空间消耗很小。

    代码实现

    // Send More Money
    // UVa ID: 10782
    // Verdict: Accepted
    // Submission Date: 2026-01-07
    // UVa Run Time: 0.480s
    //
    // 版权所有(C)2026,邱秋。metaphysis # yeah dot net

    #include <bits/stdc++.h>
    using namespace std;

    // 用于存储每个字母对应的数字
    int charToDigit[26];
    // 用于标记数字是否已被使用
    bool digitUsed[10];
    // 存储所有出现的字母
    vector<char> letters;
    // 存储每个单词
    vector<string> words;

    // 回溯法尝试所有可能的数字映射
    bool backtrack(int index) {
    // 如果所有字母都已分配数字,进行最终验证
    if (index == letters.size()) {
    // 检查是否所有单词的首字母不为0(除非单词只有一位)
    for (string &word : words)
    if (charToDigit[word[0] 'A'] == 0 && word.size() > 1)
    return false;

    // 计算所有加数(前n-1个单词)的和
    long long sum = 0;
    for (int i = 0; i < words.size() 1; ++i) {
    long long num = 0;
    for (char c : words[i])
    num = num * 10 + charToDigit[c 'A'];
    sum += num;
    }

    // 计算结果单词(最后一个单词)对应的数字
    long long result = 0;
    string lastWord = words.back();
    for (char c : lastWord)
    result = result * 10 + charToDigit[c 'A'];

    // 如果相等,则找到唯一解
    return sum == result;
    }

    char currentChar = letters[index];
    // 尝试为当前字母分配0-9中的一个未使用数字
    for (int digit = 0; digit <= 9; ++digit) {
    // 关键剪枝:如果当前字母是某个单词的首字母且单词长度>1,则不能为0
    bool isLeadingZero = false;
    if (digit == 0) {
    for (string &word : words) {
    if (word[0] == currentChar && word.size() > 1) {
    isLeadingZero = true;
    break;
    }
    }
    }
    if (isLeadingZero) continue;

    if (!digitUsed[digit]) {
    // 尝试赋值
    digitUsed[digit] = true;
    charToDigit[currentChar 'A'] = digit;
    // 递归为下一个字母赋值
    if (backtrack(index + 1))
    return true; // 找到解,直接返回
    // 回溯,撤销当前选择
    digitUsed[digit] = false;
    charToDigit[currentChar 'A'] = 1;
    }
    }
    // 当前字母尝试所有数字都失败,返回上一层
    return false;
    }

    int main() {
    int T;
    cin >> T;

    while (T) {
    int n;
    cin >> n;
    words.resize(n);
    for (int i = 0; i < n; ++i)
    cin >> words[i];

    // 初始化全局数据结构
    fill(charToDigit, charToDigit + 26, 1);
    fill(digitUsed, digitUsed + 10, false);
    letters.clear();

    // 收集所有出现的不同字母
    set<char> letterSet;
    for (string &word : words)
    for (char c : word)
    letterSet.insert(c);
    // 将set转换为vector,方便按索引访问
    letters.assign(letterSet.begin(), letterSet.end());

    // 开始回溯搜索
    backtrack(0);

    // 准备输出:将(字母,数字)对存入向量并按字母排序
    vector<pair<char, int>> result;
    for (char c : letters)
    result.push_back({c, charToDigit[c 'A']});
    sort(result.begin(), result.end());

    // 按格式输出结果
    for (size_t i = 0; i < result.size(); ++i) {
    if (i > 0) cout << ' ';
    cout << result[i].first << '=' << result[i].second;
    }
    cout << '\\n';
    }

    return 0;
    }

    代码要点说明:

    • 使用 fill 函数初始化数组,简洁高效。
    • 利用 set 自动去重和排序的特性来收集字母,再转为 vector 以便进行顺序回溯。
    • 在 backtrack 函数中,将“前导零检查”的剪枝提前到了尝试赋值的循环内部,这比在最终验证时才发现失败要高效得多。
    • 使用 long long 存储中间计算结果,防止可能的大数溢出。
    • 输出时注意格式,字母按字典序排列, = 和数字之间无空格,不同字母对之间有一个空格。

    通过这道题,我们不仅练习了回溯算法的经典应用,也学习了如何在搜索中利用问题的具体约束进行有效的剪枝,这是解决许多搜索优化问题的关键技巧。

    赞(0)
    未经允许不得转载:171主机测评 » UVa 10782 Send More Money
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址