1120. Friend Numbers

本文介绍了一种通过计算整数各位数字之和来确定“友数”及其“友ID”的方法,并提供了一个C++实现示例,用于统计一组整数中不同友ID的数量并按升序输出这些友ID。

1120. Friend Numbers (20)

时间限制
400 ms
内存限制
65536 kB
代码长度限制
16000 B
判题程序
Standard
作者
CHEN, Yue

Two integers are called "friend numbers" if they share the same sum of their digits, and the sum is their "friend ID". For example, 123 and 51 are friend numbers since 1+2+3 = 5+1 = 6, and 6 is their friend ID. Given some numbers, you are supposed to count the number of different friend ID's among them. Note: a number is considered a friend of itself.

Input Specification:

Each input file contains one test case. For each case, the first line gives a positive integer N. Then N positive integers are given in the next line, separated by spaces. All the numbers are less than 104.

Output Specification:

For each case, print in the first line the number of different frind ID's among the given integers. Then in the second line, output the friend ID's in increasing order. The numbers must be separated by exactly one space and there must be no extra space at the end of the line.

Sample Input:
8
123 899 51 998 27 33 36 12
Sample Output:
4
3 6 9 26
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<set>
#define MAXSIZE 100
using namespace std;
set<int> out;
int calSum(int N)
{
    int i=0,temp=0;
    while(N)
    {
        temp+=N%10;
        N=N/10;
        i++;
    }
    return temp;
}
int main()
{
    int N,M;
    cin>>N;
    while(N--)
    {
        cin>>M;
        int index=calSum(M);
        out.insert(index);
    }
    cout<<out.size()<<endl;
    for(set<int>::iterator it =out.begin();it!=out.end();it++)
    {
        if(it!=out.begin())
            cout<<" ";
        cout<<*it;
    }


    return 0;
}

这是一个 **数位构造 + 动态规划(DP)或 BFS 搜索** 的问题。 --- ## ✅ 问题重述 给定两个整数: - $ X $:数字 $ N $ 的各位数字之和 - $ Y $:数字 $ N $ 的各位数字的平方和 要求找出满足条件的**最小正整数 $ N $**,使得: - 所有位上的数字和为 $ X $ - 所有位上的数字平方和为 $ Y $ 如果不存在这样的数,或者这个数的位数超过 100,则输出 `"No solution"`。 如果有多个解,返回**字典序最小的数**(也就是数值最小的数)。 --- ## 🔍 关键分析 ### 数字范围 每个数位只能是 `0` 到 `9`,但注意: - 数字不能以 `0` 开头(除非整个数就是 `0`,但题目中 $ X \geq 1 $,所以最小也是 `1`) 每个数字 $ d \in [0,9] $,对应的贡献: - 和:$ d $ - 平方和:$ d^2 $ 我们希望用最少的位数、并且从小到大排列(为了最小化结果),来达到目标 $ (X, Y) $ --- ## ✅ 解法思路 这本质上是一个 **双约束的完全背包问题**,其中: - 物品:数字 0~9(但我们不会选 0 作为首位) - 每个数字可以重复使用 - 背包容量是二维:总和 $ X $,平方和 $ Y $ - 目标:找到一组数字(可排序),使得组合成的数字最小 但由于我们要的是**最小的整数**,即 **字典序最小**,而字典序最小意味着高位尽可能小 → 所以我们应该尽量把小的数字放前面?不对! ⚠️ 注意:`19 < 91`,但 `1` 在前。所以为了得到最小整数,我们应该让**较小的数字出现在较高位**。 但是!如果我们先生成所有可能的数字组合,然后排序 → 太慢。 更好的方法是: > 使用 **BFS 或 DP** 来寻找从 $ (0,0) $ 到 $ (X,Y) $ 的最短路径(按位数最少优先,再按字典序最小) 但我们更关心的是:如何构造出**字典序最小的数字串** --- ### 正确策略:动态规划 + 路径还原 我们可以使用 **DP[x][y] = 最小字符串(或前驱信息)** 表示能否达到状态 $ (x,y) $ 但由于 $ X, Y \leq 10000 $,直接开二维数组存字符串会爆内存。 替代方案: - 使用 `dp[x][y]` 存储到达 $ (x,y) $ 的**最小字符串**(只保留最优的一个) - 但我们仍然需要优化:最多 100 位,且每位最大贡献和为 9,平方和为 81 所以最大能表示的和是 $ 100 \times 9 = 900 $,平方和 $ 100 \times 81 = 8100 $ 但题目中 $ X, Y \leq 10000 $,说明有些输入根本无解(比如 $ X=10000 $) 所以我们可以在预处理时限制搜索范围:最多 100 位 → 所以 $ x \leq 900 $, $ y \leq 8100 $ 因此,当 $ X > 900 $ 或 $ Y > 8100 $,可以直接判 `"No solution"` --- ## ✅ 算法步骤 1. 如果 $ X > 900 $ 或 $ Y > 8100 $ → 输出 `"No solution"` 2. 使用 BFS 层次遍历(按位数递增)或 DP 枚举所有可达状态 $ (x, y) $ 3. 对每个状态记录:**到达该状态的最小字符串** 4. 使用队列进行 BFS,初始状态为 "1" ~ "9"(避免前导零) 5. 每次尝试在当前字符串后添加一个数字(0~9),更新新状态 $ (x', y') $ 6. 只有当新的字符串长度 ≤ 100,并且新状态未被更优方式访问过时才入队 但 BFS 字符串太多,状态空间太大。 --- ### 更优做法:贪心构造 + DFS 回溯?不行。 --- ### 最佳做法:**反向 DP + 构造最小答案** 我们考虑:要使最终数字最小,应尽可能多地使用小数字(如 1,2,...),而且高位要小。 但我们可以换一种方式: > 枚举每一位填什么数字(从高位到低位),从 `1` 开始试到 `9` 作为第一位,然后允许后面填 `0` 但这也不容易。 --- ### 成功做法:**完全背包 + 路径还原(按字典序最小)** 参考经典题:“给定位数和与平方和,构造最小数” 我们采用如下策略: #### 思路: 由于数字顺序不影响总和和平方和,只影响大小 → 我们希望数字的各位是**非降序排列**(即排序后最小) 但注意:`113 < 131 < 311` → 所以最小的数一定是其各位数字**单调不降** 👉 所以我们只需枚举每种数字用了多少个(计数),然后拼接成非降序列即可。 这就变成了一个 **整数线性组合问题**: 找非负整数 $ c_0, c_1, ..., c_9 $,使得: $$ \sum_{d=0}^9 c_d \cdot d = X \\ \sum_{d=0}^9 c_d \cdot d^2 = Y \\ \sum_{d=0}^9 c_d \leq 100 \quad (\text{最多100位}) $$ 并且 $ c_0 $ 可以为任意值(但不能作为首字符) 然后我们想让组成的数字最小 → 所以应该将数字从小到大排列,但如果 `0` 出现了,必须放在后面(但不是开头) 不过因为我们要最小化整数,所以应该: - 首位取最小的非零数字 - 后续补上所有 `0` - 再补其他数字升序排列 例如:若用了两个 `0`,一个 `1`,一个 `3` → 应该是 `1003` 所以算法变为: 1. 枚举所有可能的数字频次组合(DFS 或 DP) 2. 找到所有满足条件的组合 3. 构造对应数字并取最小 4. 若没有合法组合,输出 "No solution" 但枚举频次组合太慢。 --- ## ✅ 推荐做法:动态规划 + 路径追踪(BFS with pruning) 我们使用 **BFS 按照位数扩展**,状态为 $ (x, y) $,记录到达该状态的**最小字符串** ### 优化点: - 状态总数:$ x \in [0, 900], y \in [0, 8100] $ → 最多约 7e6 个状态 - 每个状态维护一个字符串?内存爆炸 不行。 --- ## ✅ 实际可行做法:**DP[x][y] = 到达(x,y)的最小字符串长度,并记录前驱** 改为:**记录路径来源** 定义: ```cpp dist[x][y] = 达到和为 x、平方和为 y 的最小位数 parent[x][y] = 是由哪个数字转移而来 from_x[x][y], from_y[x][y] = 前一个状态 ``` 但这样无法保证字典序最小。 --- ## ✅ 终极正确做法:**逐位构造 + BFS(优先队列)** 使用 **优先队列(最小堆)**,每次取出当前字符串最小的候选,尝试在其末尾加一位数字(0~9),直到找到目标 $ (X,Y) $ 因为我们在扩展时总是处理字典序更小的串,第一个到达 $ (X,Y) $ 的就是答案。 同时剪枝:如果当前长度 ≥ 100,不再扩展 --- ## ✅ C++ 实现代码(优先队列 BFS) ```cpp #include <iostream> #include <queue> #include <string> #include <vector> #include <algorithm> #include <climits> using namespace std; struct State { string num; int sum; int sq_sum; bool operator<(const State& other) const { // 优先队列按字符串字典序升序 return num > other.num; } }; string solve(int X, int Y) { if (X > 900 || Y > 8100) return "No solution"; // 超出最大可能 if (X == 0) return Y == 0 ? "0" : "No solution"; priority_queue<State> pq; vector<vector<bool>> visited(901, vector<bool>(8101, false)); // 初始状态:1~9 for (int d = 1; d <= 9; ++d) { int s = d; int ss = d * d; if (s <= 900 && ss <= 8100) { pq.push({to_string(d), s, ss}); } } while (!pq.empty()) { State cur = pq.top(); pq.pop(); // 剪枝:已访问过此状态(且已有更短或更小的路径) if (visited[cur.sum][cur.sq_sum]) continue; visited[cur.sum][cur.sq_sum] = true; // 检查是否达到目标 if (cur.sum == X && cur.sq_sum == Y) { return cur.num; } // 如果已经 100 位了,不能再扩展 if (cur.num.size() >= 100) continue; // 尝试添加 0~9 for (int d = 0; d <= 9; ++d) { int ns = cur.sum + d; int nss = cur.sq_sum + d * d; if (ns > 900 || nss > 8100) continue; if (visited[ns][nss]) continue; string new_num = cur.num + char('0' + d); pq.push({new_num, ns, nss}); } } return "No solution"; } int main() { int X, Y; while (cin >> X >> Y) { cout << solve(X, Y) << endl; } return 0; } ``` --- ## ✅ 示例验证 ### 输入 1: `1 1` - 需要 sum=1, sq_sum=1 → 数字 '1' → 1²=1 → ✔️ - 输出 `1` ✔️ ### 输入 2: `10000 100` - X=10000 > 900 → 直接 `"No solution"` ✔️ ### 输入 3: `9 81` - sum=9, sq_sum=81 → 唯一可能是单个数字 9 → 9=9, 81=9² → ✔️ - 输出 `9` ✔️ 其他测试: 比如 `10 2`: 只能是 1111111111(十个1)→ sum=10, sq_sum=10 ≠2 → 不行 或 `2 2`: 用两个1:11 → sum=2, sq_sum=2 → 所以答案是 `11` 程序会从 "1" → 扩展 "10", "11", ... 其中 "11" 会进入队列并在满足条件时返回。 但注意:"2" 本身 sum=2, sq_sum=4 → 不符合 所以 `2 2` 应该是 `11` --- ## ✅ 复杂度分析 - 状态数:最多 $ 901 \times 8101 \approx 7.3e6 $ - 每个状态扩展 10 次 - 优先队列操作 $ O(\log n) $ - 总体时间复杂度较高,但在实际中由于剪枝(只访问每个状态一次)+ 优先返回第一个解,表现尚可 对于 $ X, Y $ 较大的情况,可能无解,也会快速退出 --- ## ✅ 改进方向(竞赛级) 使用 **DP[x][y] = 最小可能字符串(仅存储最优)**,用滚动数组 + 字符串哈希优化,但本题 $ X,Y $ 上限高,但有效范围有限 另一种方法是:**数学构造法** 观察:为了让数字最小,应尽可能多用小数字(如 1,2,3...),尤其是 `1`(因为平方增长慢) 可以尝试贪心:先确定位数 $ k $,然后用 DP 求是否存在组合,再构造最小字符串 但上述 BFS 方法逻辑清晰、正确性高 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值