3348. Smallest Divisible Digit Product II
題目 / Problem
中文: 給你一個表示正整數的字串 num,以及一個整數 t。如果一個數字的每一位都不是 0,就稱它為「無零數 (zero-free)」。請回傳一個字串,代表大於等於 num 的最小無零數,且其各位數字的乘積能被 t 整除。如果不存在這樣的數,回傳 "-1"。
English: You are given a string num representing a positive integer and an integer t. A number is zero-free if none of its digits is 0. Return the smallest zero-free number ≥ num whose product of digits is divisible by t. If no such number exists, return "-1".
Constraints:
- 2 <= num.length <= 2 * 10^5
- num consists only of digits '0'–'9', with no leading zeros.
- 1 <= t <= 10^14
Worked example: num = "1234", t = 256. Answer "1488". The digit product 1·4·8·8 = 256, which is divisible by 256 = 2^8, and 1488 is the smallest zero-free number ≥ 1234 achieving this.
名詞解釋 / Glossary
- 質因數分解 / Prime factorization: 把一個整數寫成質數相乘的形式,例如
256 = 2^8。這裡我們只關心質因數 2、3、5、7。 / Writing an integer as a product of primes; here we only care about the primes 2, 3, 5, 7. - 數字乘積的質因數 / Prime factors of a digit product: 因為每個數字 1–9 只由質因數 2、3、5、7 組成,整個乘積也只含這四個質因數。 / Since every digit 1–9 is built only from primes 2, 3, 5, 7, so is any product of digits.
- 指數計數 / Exponent counting: 乘積可能是天文數字,我們不存乘積本身,而是分別記錄質因數 2、3、5、7 的指數(出現次數)。 / The product is astronomically large, so instead of storing it we track the exponent (count) of each prime 2, 3, 5, 7.
- 貪心 / Greedy: 每一步都做「當下看起來最好」的選擇(例如盡量保留前綴、盡量放小的數字)。 / Making the locally best choice at each step (keep the longest prefix, place the smallest digit).
- 前綴和 / Prefix sum: 預先算好「前
i位數字」累積的質因數指數,之後可 O(1) 查詢任意前綴。 / Precomputed cumulative prime exponents of the firstidigits so any prefix can be queried in O(1). - 無零數 / Zero-free number: 每一位都在 1–9 之間、沒有 0 的數。 / A number all of whose digits lie in 1–9.
思路
中文:
先看暴力法:從 num 開始一個一個往上數,檢查每個數是否無零且乘積被 t 整除。但 num 可能有 20 萬位,這完全不可行。
第一個關鍵觀察(提示 1):數字 1–9 的質因數只有 2、3、5、7。所以任何數字乘積也只含這四個質因數。若把 t 除盡 2、3、5、7 後還剩下大於 1 的部分(例如 26 = 2·13 中的 13),那永遠不可能被整除,直接回傳 "-1"。分解後得到需要的指數 need = (e2, e3, e5, e7):乘積要合格,各質因數的總指數必須分別 ≥ 這些值。
再觀察:每個數字對指數的貢獻很小(8 最多給三個 2、9 最多給兩個 3、5 只給一個 5、7 只給一個 7),因此「湊出 t」所需的最少位數 m 非常小(t ≤ 10^14 時 m ≤ 20 左右)。這意味著答案要嘛和 num 一樣長,要嘛只比它長一點。
於是策略分三塊。(1) 若 num 本身無零且乘積已合格,它就是答案。(2) 否則找「和 num 同長度、且 > num 的最小合格數」:枚舉一個「增大位置 p」——前綴 num[0..p-1] 保持不變,把第 p 位改成一個比 num[p] 大的數字 d,後面的位隨意填成最小的合格後綴。保留越長的前綴,數字越小,所以我們從右往左掃 p,取最右邊仍可行的位置。可行性判斷只需:扣掉前綴與 d 貢獻後的剩餘指數,其最少位數 ≤ 剩下的空位數。用前綴和可 O(1) 判斷。(3) 若同長度做不出來,就用長度 L+1(或最小可行長度):前面補 1、後面放湊出 t 的最小數字。
填後綴時,「最小」的規則是:先湊出剩餘指數所需的最少位數(多的空位全填 1,放最前面因為 1 最小),再把那些數字由小到大排列。湊指數的貪心:3 的指數用 9(兩個 3)、2 的指數用 8(三個 2),剩下的零頭用一張小表(例如剩一個 2 一個 3 就用 6)補齊。
English:
Brute force — counting up from num and testing each number — is hopeless when num has up to 200,000 digits.
First key insight (hint 1): digits 1–9 only carry the primes 2, 3, 5, 7, so any digit product does too. Strip all factors of 2, 3, 5, 7 out of t; if anything greater than 1 remains (e.g. the 13 in 26 = 2·13), no product can ever be divisible by t, so return "-1". The stripping gives the required exponents need = (e2, e3, e5, e7): to be valid, the digit product's total exponent of each prime must be at least these.
Second insight: each digit contributes tiny exponents (8 gives at most three 2's, 9 at most two 3's, 5 exactly one 5, 7 exactly one 7), so the minimum number of digits m needed to realise t is very small (m ≤ ~20 for t ≤ 10^14). Hence the answer is either the same length as num or barely longer.
The plan has three parts. (1) If num is already zero-free and valid, return it. (2) Otherwise find the smallest valid number of the same length that is > num: pick an "increase position p" — keep the prefix num[0..p-1], raise digit p to some d > num[p], and fill the rest with the smallest valid suffix. A longer kept prefix means a smaller number, so scan p from right to left and take the right-most feasible one. Feasibility is just: the residual exponents (after subtracting the prefix and d) need at most as many digits as the empty slots remaining — checkable in O(1) using prefix sums. (3) If no same-length answer exists, use length L+1 (or the minimum feasible length): pad the front with 1's and place the minimal t-covering digits at the back.
To fill a suffix minimally: cover the residual exponents with the fewest digits (fill leftover slots with 1's up front, since 1 is smallest), then sort those digits ascending. The exponent-covering greedy: use 9 for pairs of 3's, 8 for triples of 2's, and a tiny lookup table for the leftovers (e.g. one leftover 2 and one leftover 3 become a single 6).
逐步走查 / Walkthrough
Input num = "1234", t = 256.
Setup / 準備: 256 = 2^8, so need = (e2=8, e3=0, e5=0, e7=0). No leading zero issues (num has no '0'). Digit exponents of 2: 1→0, 2→1, 3→0, 4→2, total = 3 < 8, so num itself is not valid → must find something > 1234.
Prefix sums of the 2-exponent: pre(0)=0, pre(1)=0 (digit 1), pre(2)=1 (digit 2), pre(3)=1 (digit 3), pre(4)=3 (digit 4). Scan p from 3 down to 0 (pmax = 3), each time trying the smallest d > num[p].
| p | kept prefix | pre2(p) | slots = L−1−p | try d (>num[p]) | residual 2-exp = 8−pre2−exp2(d) | min digits needed | ≤ slots? |
|---|---|---|---|---|---|---|---|
| 3 | "123" | 1 | 0 | 5,6,7,8,9 | best case d=8 → 8−1−3 = 4 | 2 | ✗ (need 0) |
| 2 | "12" | 1 | 1 | 4..9 | best d=8 → 8−1−3 = 4 | 2 | ✗ (>1) |
| 1 | "1" | 0 | 2 | d=4 → 8−0−2 = 6 | 6 → two 8's = 2 digits | 2 | ✓ |
At p = 1, digit d = 4 (smallest d > 2 making it feasible) works: residual is 2^6, and the 2 empty slots can hold two 8's (8·8 = 2^6). Since p = 3 and p = 2 failed, p = 1 is the right-most feasible position.
Build / 組裝: prefix "1" + increased digit "4" + fill 2 slots for residual 2^6 → fillSuffix gives "88" (two 8's, zero padding). Result "1" + "4" + "88" = "1488". ✓
Solution — C
#include <stdlib.h> // malloc / 動態配置記憶體
#include <string.h> // strlen, memcpy / 字串長度與複製
// 回傳數字 d(1..9) 的質因數 2,3,5,7 指數 / prime exponents (2,3,5,7) of digit d
static void digExp(int d, int *a2, int *a3, int *a5, int *a7) {
int e2 = 0, e3 = 0, e5 = 0, e7 = 0; // 預設全為 0 / default all zero
switch (d) { // 依數字查表 / lookup by digit
case 2: e2 = 1; break; // 2 = 2^1
case 3: e3 = 1; break; // 3 = 3^1
case 4: e2 = 2; break; // 4 = 2^2
case 5: e5 = 1; break; // 5 = 5^1
case 6: e2 = 1; e3 = 1; break; // 6 = 2·3
case 7: e7 = 1; break; // 7 = 7^1
case 8: e2 = 3; break; // 8 = 2^3
case 9: e3 = 2; break; // 9 = 3^2
default: break; // 1(或0) 不貢獻 / 1 (or 0) contributes nothing
}
*a2 = e2; *a3 = e3; *a5 = e5; *a7 = e7; // 用指標把結果寫回呼叫端 / write back via pointers
}
// 覆蓋剩餘指數所需的「最少位數」/ minimum digit count to cover the residual exponents
static int minDigits(int r2, int r3, int r5, int r7) {
if (r2 < 0) r2 = 0; if (r3 < 0) r3 = 0; // 負數代表已滿足,視為 0 / clamp negatives to 0
if (r5 < 0) r5 = 0; if (r7 < 0) r7 = 0;
int e8 = r2 / 3, rem2 = r2 % 3; // 每個 8 吃掉三個 2 / each 8 absorbs three 2's
int n9 = r3 / 2, rem3 = r3 % 2; // 每個 9 吃掉兩個 3 / each 9 absorbs two 3's
int lc; // 零頭需要的位數 / digits for the leftovers
if (rem2 == 0 && rem3 == 0) lc = 0; // 無零頭 / nothing left
else if (rem2 == 2 && rem3 == 1) lc = 2; // 2^2·3 → 需兩位(如 2,6) / needs two digits
else lc = 1; // 其餘情形一位即可 / one digit otherwise
return e8 + n9 + lc + r5 + r7; // 5 與 7 各只能靠 '5','7' / only '5','7' give 5,7
}
// 把「長度剛好 k、覆蓋剩餘指數的最小字串」寫進 dest / write smallest length-k covering string
static void fillSuffix(char *dest, int k, int r2, int r3, int r5, int r7) {
if (r2 < 0) r2 = 0; if (r3 < 0) r3 = 0; // 同樣把負值歸零 / clamp negatives
if (r5 < 0) r5 = 0; if (r7 < 0) r7 = 0;
int c8 = r2 / 3, rem2 = r2 % 3; // 8 的個數與 2 的零頭 / count of 8's, leftover 2's
int c9 = r3 / 2, rem3 = r3 % 2; // 9 的個數與 3 的零頭 / count of 9's, leftover 3's
int c2 = 0, c3 = 0, c4 = 0, c6 = 0; // 零頭湊出的小數字 / small digits from leftovers
if (rem2 == 1 && rem3 == 0) c2 = 1; // 剩一個 2 → '2'
else if (rem2 == 2 && rem3 == 0) c4 = 1; // 剩兩個 2 → '4'(=2^2 只花一位) / one digit
else if (rem2 == 0 && rem3 == 1) c3 = 1; // 剩一個 3 → '3'
else if (rem2 == 1 && rem3 == 1) c6 = 1; // 剩一個2一個3 → '6'(併成一位) / merge into 6
else if (rem2 == 2 && rem3 == 1) { c2 = 1; c6 = 1; } // 2^2·3 → "26" 最小 / smallest
int c5 = r5, c7 = r7; // 5 和 7 直接放 / place 5's and 7's directly
int m = c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9; // 有效數字總數 / count of non-1 digits
int idx = 0; // 目前寫到的位置 / current write index
for (int i = 0; i < k - m; i++) dest[idx++] = '1'; // 多的位補 '1' 放最前(最小) / pad 1's
for (int i = 0; i < c2; i++) dest[idx++] = '2'; // 之後由小到大輸出 / then ascending
for (int i = 0; i < c3; i++) dest[idx++] = '3';
for (int i = 0; i < c4; i++) dest[idx++] = '4';
for (int i = 0; i < c5; i++) dest[idx++] = '5';
for (int i = 0; i < c6; i++) dest[idx++] = '6';
for (int i = 0; i < c7; i++) dest[idx++] = '7';
for (int i = 0; i < c8; i++) dest[idx++] = '8';
for (int i = 0; i < c9; i++) dest[idx++] = '9'; // idx 最終等於 k / idx ends at k
}
/* 演算法 / Algorithm:
1) 把 t 除盡 2,3,5,7;若有殘餘>1 則無解回 "-1"。
2) num 本身無零且乘積合格就直接回傳。
3) 否則從右往左找「增大位置」湊出同長度的最小合格數;找不到就用長度 L+1。*/
char* smallestNumber(char* num, long long t) {
int L = strlen(num); // num 的位數 / number of digits
long long tt = t; // 複製 t 來做分解 / copy for factoring
int e2 = 0, e3 = 0, e5 = 0, e7 = 0; // 需要的質因數指數 / required exponents
while (tt % 2 == 0) { tt /= 2; e2++; } // 除盡 2 / strip factors of 2
while (tt % 3 == 0) { tt /= 3; e3++; } // 除盡 3
while (tt % 5 == 0) { tt /= 5; e5++; } // 除盡 5
while (tt % 7 == 0) { tt /= 7; e7++; } // 除盡 7
if (tt != 1) { // 還有其他質因數 → 不可能整除 / impossible
char *r = malloc(3); strcpy(r, "-1"); return r; // 配置並回傳 "-1"
}
int z = L; // 第一個 '0' 的位置(沒有則 = L) / first zero index
for (int i = 0; i < L; i++) if (num[i] == '0') { z = i; break; }
long long T2 = 0, T3 = 0, T5 = 0, T7 = 0; // 整個 num 的指數總和 / totals over all digits
for (int i = 0; i < L; i++) {
int a2, a3, a5, a7; digExp(num[i] - '0', &a2, &a3, &a5, &a7); // num[i]-'0' 由字元轉數字
T2 += a2; T3 += a3; T5 += a5; T7 += a7;
}
if (z == L && T2 >= e2 && T3 >= e3 && T5 >= e5 && T7 >= e7) { // num 無零且已合格 / valid
char *r = malloc(L + 1); memcpy(r, num, L); r[L] = '