下一个更大的数值平衡数 图解题解
这道题到底在问什么
- 输入
- n = 1
- 输出
- 22(数位 2 出现 2 次)
- 输入
- n = 1000
- 输出
- 1333(1 出现 1 次,3 出现 3 次)
- 输入
- n = 13
- 输出
- 22(本节演示用例)
先想最直接的笨办法
记牢这一句:从 n 的下一个数开始,挨个试。判定一个数平不平衡,就看它每个数位 d 是不是恰好出现 d 次。下面从 14 开始,一步一步试到 22。(动画第 3 步)
最优解:一步一步想明白
- 3记牢这一句:从 n 的下一个数开始,挨个试。判定一个数平不平衡,就看它每个数位 d 是不是恰好出现 d 次。下面从 14 开始,一步一步试到 22。
- 4枚举起点 14这一排就是我们要挨个验的候选,从 n 加 1 也就是 14 开始,一直往上。算法很朴素:排头这个先试,不平衡就划掉换下一个,直到撞上第一个平衡数。紫色高亮的就是当前正在试的候选,现在轮到 14。
- 5统计 14 的数位轮到候选 14。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。14 拆出来是 1 和 4,统计下来 数位 1 出现 1 次,数位 4 出现 1 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 614 淘汰判定 14。数位 1 那一位是对上了,数位 4 出现 1 次,却需要出现 4 次,这一位对不上,整个数就不平衡。把 14 划掉,枚举指针往前挪一格,去试 15。
- 7统计 15 的数位轮到候选 15。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。15 拆出来是 1 和 5,统计下来 数位 1 出现 1 次,数位 5 出现 1 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 815 淘汰判定 15。数位 1 那一位是对上了,数位 5 出现 1 次,却需要出现 5 次,这一位对不上,整个数就不平衡。把 15 划掉,枚举指针往前挪一格,去试 16。
- 9统计 16 的数位轮到候选 16。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。16 拆出来是 1 和 6,统计下来 数位 1 出现 1 次,数位 6 出现 1 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 1016 淘汰判定 16。数位 1 那一位是对上了,数位 6 出现 1 次,却需要出现 6 次,这一位对不上,整个数就不平衡。把 16 划掉,枚举指针往前挪一格,去试 17。
- 11统计 17 的数位轮到候选 17。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。17 拆出来是 1 和 7,统计下来 数位 1 出现 1 次,数位 7 出现 1 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 1217 淘汰判定 17。数位 1 那一位是对上了,数位 7 出现 1 次,却需要出现 7 次,这一位对不上,整个数就不平衡。把 17 划掉,枚举指针往前挪一格,去试 18。
- 13统计 18 的数位轮到候选 18。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。18 拆出来是 1 和 8,统计下来 数位 1 出现 1 次,数位 8 出现 1 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 1418 淘汰判定 18。数位 1 那一位是对上了,数位 8 出现 1 次,却需要出现 8 次,这一位对不上,整个数就不平衡。把 18 划掉,枚举指针往前挪一格,去试 19。
- 15统计 19 的数位轮到候选 19。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。19 拆出来是 1 和 9,统计下来 数位 1 出现 1 次,数位 9 出现 1 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 1619 淘汰判定 19。数位 1 那一位是对上了,数位 9 出现 1 次,却需要出现 9 次,这一位对不上,整个数就不平衡。把 19 划掉,枚举指针往前挪一格,去试 20。
- 17已淘汰 14-19试到这里停一下看看进度。14 到 19 这六个已经全被划成灰色,它们淘汰的原因其实一模一样:除了数位 1 是出现 1 次刚好达标,另一个数位比如 4 到 9,都只出现了 1 次,可它们要求出现自己那么多次,次数远远不够。接着往下,轮到 20,这个数里藏着 0,会更干脆地出局。
- 18统计 20 的数位轮到候选 20。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。20 拆出来是 2 和 0,统计下来 数位 0 出现 1 次,数位 2 出现 1 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 1920 淘汰判定 20。数位 2 出现 1 次,却需要出现 2 次,这一位对不上,整个数就不平衡。注意数位 0:它一出现就必然违规,因为平衡数要求 0 出现 0 次,自相矛盾。把 20 划掉,枚举指针往前挪一格,去试 21。
- 20统计 21 的数位轮到候选 21。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。21 拆出来是 2 和 1,统计下来 数位 1 出现 1 次,数位 2 出现 1 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 2121 淘汰判定 21。数位 1 那一位是对上了,数位 2 出现 1 次,却需要出现 2 次,这一位对不上,整个数就不平衡。把 21 划掉,枚举指针往前挪一格,去试 22。
- 22统计 22 的数位轮到候选 22。先把它拆成一个个数位,每碰到一个数位就在计数桶 cnt 里给对应的格子加一。22 拆出来是 2 和 2,统计下来 数位 2 出现 2 次。侧栏这张小表就是 cnt,记录了每个出现过的数位各出现了几次。
- 2322 命中判定 22。它只有数位 2,而 2 恰好出现 2 次,cnt[2] 等于 2,完全配平。这是我们从 14 一路试上来,第一个满足条件的数。算法在这里立刻停手,把 22 返回,它就是严格大于 13 的最小平衡数。
- 2422 复核通过再复核一眼,确认没漏判。cnt 这张表里只有数位 2 亮着,值是 2,其余从 0 到 9 的格子全是 0,没有任何出现了却次数对不上的数位,也没有 0 来捣乱。条件干干净净地满足,答案就锁定在 22。
- 25答案 = 22回看整排候选。14 到 21 这八个,要么某个数位次数不够,要么含了 0,全被划成灰色淘汰掉;只有排在最后的 22 一路配平,亮成绿色。枚举从 14 走到 22,撞上第一个平衡数就返回,答案就是 22,和一开始说的对上了。
⚠️ 容易写错的地方
✗ 错:忘了平衡数不能含 0
✓ 对:含 0 的候选一律淘汰
数位 0 一旦出现,就要求 0 出现 0 次,自相矛盾,所以合法平衡数里绝不会有 0,像 1022 就因为多了个 0 被排除
✗ 错:把「大于等于 n」当成答案条件
✓ 对:必须严格大于 n
题目要的是严格大于 n 的最小平衡数,枚举从 n 加 1 起,哪怕 n 本身就是平衡数也不能返回它
✗ 错:只检查出现过的数位,漏了「次数刚好」
✓ 对:每个出现的数位都要次数恰好等于自身
不是出现就行,是出现几次要严格等于这个数位的值,11 里 1 出现 2 次就不合格
✗ 错:担心枚举永远找不到答案
✓ 对:答案一定存在且有界
像 1224444 这类平衡数(1 出现 1 次、2 出现 2 次、4 出现 4 次)在不太远处必然出现,枚举一定会在有限步内命中,不会死循环
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def nextBeautifulNumber(self, n: int) -> int:
for x in count(n + 1):
y = x
cnt = [0] * 10
while y:
y, v = divmod(y, 10)
cnt[v] += 1
if all(v == 0 or i == v for i, v in enumerate(cnt)):
return xC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int nextBeautifulNumber(int n) {
for (int x = n + 1;; ++x) {
int cnt[10]{};
for (int y = x; y > 0; y /= 10) {
++cnt[y % 10];
}
bool ok = true;
for (int y = x; y > 0; y /= 10) {
if (y % 10 != cnt[y % 10]) {
ok = false;
break;
}
}
if (ok) {
return x;
}
}
}
};Java
import java.util.*;
class Solution {
public int nextBeautifulNumber(int n) {
for (int x = n + 1;; ++x) {
int[] cnt = new int[10];
for (int y = x; y > 0; y /= 10) {
++cnt[y % 10];
}
boolean ok = true;
for (int y = x; y > 0; y /= 10) {
if (y % 10 != cnt[y % 10]) {
ok = false;
break;
}
}
if (ok) {
return x;
}
}
}
}复杂度
时间
O(k · d)
k 是从 n+1 到答案要枚举的候选个数,d 是每个候选的位数。数值范围内 n 不超过一百万,答案有界(平衡数分布够密,最坏也就跳到七位数附近),d 至多 7 位是常数。所以整体近似线性于要试的候选数
空间
O(1)
只用一个长度固定为 10 的计数桶 cnt,和几个循环变量。不随 n 增长开更多空间,也没有递归栈。判定完一个候选就把 cnt 清空复用
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 下一个更大的数值平衡数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题最直接的做法是什么?+
暴力枚举。从 n 加 1 开始,x 一个个往上加,对每个 x 数出每个数位出现的次数,再检查是不是每个出现的数位 d 都恰好出现 d 次。第一个满足的就是答案。因为数据范围内答案有界,枚举一定能在有限步命中。
暴力会不会太慢,有没有更快的思路?+
数据范围里 n 不超过一百万,答案顶多到七位数附近,枚举加数位计数完全跑得动,面试里直接写暴力是稳妥选择。想更快可以预先构造:枚举哪些数位以及各自的出现次数组合,用回溯或全排列拼出所有平衡数,排序后二分找第一个大于 n 的。工程上暴力已经够用。
判定时有什么坑要特别小心?+
两个:一是必须严格大于 n,枚举从 n 加 1 起,n 自己是平衡数也不能要;二是含 0 的数一律不合法,因为 0 出现就得出现 0 次,自相矛盾。写判定时盯住这两点基本不会错。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 下一个更大的数值平衡数 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。