LeetCode难题解析:数字字符串的平衡排列数目
问题描述
给定一个数字字符串 num
,统计其所有不同排列中满足以下条件的数目:
奇数位下标数字之和 = 偶数位下标数字之和(下标从0开始)。
答案需对 109+7109+7 取模。
示例
输入:num = "08143"
输出:12
解释:存在12种排列方式使得奇数位和与偶数位和相等。
初探问题:暴力法的局限
思路分析
最直观的想法是生成所有排列,逐一检查是否满足条件。然而,当字符串长度 nn 较大时,排列数为 O(n!)O(n!),显然会超时。
代码示例(Java)
java
// 暴力回溯(仅示意,不可行) public class BruteForceSolution {private int count = 0;public int countBalancedPermutations(String num) {backtrack(num.toCharArray(), 0);return count;}private void backtrack(char[] arr, int start) {if (start == arr.length) {if (isBalanced(arr)) count++;return;}for (int i = start; i < arr.length; i++) {swap(arr, start, i);backtrack(arr, start + 1);swap(arr, start, i);}}private boolean isBalanced(char[] arr) {int evenSum = 0, oddSum = 0;for (int i = 0; i < arr.length; i++) {if (i % 2 == 0) evenSum += arr[i] - '0';else oddSum += arr[i] - '0';}return evenSum == oddSum;} }
缺点:时间复杂度 O(n!)O(n!),无法处理 n>10n>10 的情况。
优化思路:组合数学 + 动态规划
关键观察
-
总和奇偶性:若所有数字之和为奇数,直接返回0。
-
分配问题:将数字分配到奇数位和偶数位,使得两部分和相等。
数学建模
设总共有 mm 个奇数位和 kk 个偶数位(m=⌈n/2⌉m=⌈n/2⌉, k=⌊n/2⌋k=⌊n/2⌋)。
对每个数字 dd,选择 jj 个放在奇数位,剩余 cnt[d]−jcnt[d]−j 个放在偶数位,需满足:
-
∑(jd×d)=总和2∑(jd×d)=2总和
-
∑jd=m∑jd=m
动态规划状态定义
定义 dfs(pos, currSum, oddRemain)
:
-
pos
:当前处理的数字(0~9) -
currSum
:奇数位当前总和 -
oddRemain
:剩余可分配的奇数位数量
转移方程
对每个数字 dd,枚举分配到奇数位的数量 jj,满足:
j∈[max(0,cnt[d]−剩余偶数位),min(cnt[d],oddRemain)]j∈[max(0,cnt[d]−剩余偶数位),min(cnt[d],oddRemain)]
方案数为组合数乘积:
C(oddRemain,j)×C(剩余偶数位,cnt[d]−j)C(oddRemain,j)×C(剩余偶数位,cnt[d]−j)
高效实现:预计算与记忆化
预计算组合数
使用动态规划预计算组合数 C(n,k)mod MODC(n,k)modMOD,避免重复计算。
java
// 预处理组合数C(n,k) comb = new long[maxOdd + 1][maxOdd + 1]; for (int i = 0; i <= maxOdd; i++) {comb[i][0] = 1;for (int j = 1; j <= i; j++) {comb[i][j] = (comb[i-1][j] + comb[i-1][j-1]) % MOD;} }
记忆化搜索
通过三维数组 memo[pos][currSum][oddRemain]
记录状态,减少重复计算。
java
private long dfs(int pos, int currSum, int oddRemain) {if (memo[pos][currSum][oddRemain] != -1) return memo[pos][currSum][oddRemain];// ... 递归逻辑 ...memo[pos][currSum][oddRemain] = res;return res; }
剪枝优化
-
若当前总和超过目标值
target
,提前返回。 -
若剩余奇数位数量不足,跳过非法状态。
完整代码实现
java
import java.util.Arrays;public class Solution {private static final long MOD = 1_000_000_007;private long[][][] memo;private int[] cnt;private int target;private long[][] comb;private int[] psum;public int countBalancedPermutations(String num) {// 统计数字频率并计算总和cnt = new int[10];int tot = 0, n = num.length();for (char c : num.toCharArray()) {int d = c - '0';cnt[d]++;tot += d;}if (tot % 2 != 0) return 0;target = tot / 2;int maxOdd = (n + 1) / 2;// 预处理组合数comb = new long[maxOdd + 1][maxOdd + 1];for (int i = 0; i <= maxOdd; i++) {comb[i][0] = 1;for (int j = 1; j <= i; j++) {comb[i][j] = (comb[i-1][j] + comb[i-1][j-1]) % MOD;}}// 计算psum[i]: 从i到9的数字总数psum = new int[11];for (int i = 9; i >= 0; i--) {psum[i] = psum[i+1] + cnt[i];}// 初始化记忆化数组memo = new long[10][target+1][maxOdd+1];for (long[][] arr2 : memo) {for (long[] arr1 : arr2) {Arrays.fill(arr1, -1);}}return (int) dfs(0, 0, maxOdd);}private long dfs(int pos, int currSum, int oddRemain) {if (oddRemain < 0 || currSum > target) return 0;if (pos == 10) {return (currSum == target && oddRemain == 0) ? 1 : 0;}if (memo[pos][currSum][oddRemain] != -1) {return memo[pos][currSum][oddRemain];}long res = 0;int evenRemain = psum[pos] - oddRemain;int lower = Math.max(0, cnt[pos] - evenRemain);int upper = Math.min(cnt[pos], oddRemain);for (int j = lower; j <= upper; j++) {int k = cnt[pos] - j;if (k < 0 || k > evenRemain) continue;long ways = (comb[oddRemain][j] * comb[evenRemain][k]) % MOD;res = (res + ways * dfs(pos+1, currSum + pos*j, oddRemain - j)) % MOD;}memo[pos][currSum][oddRemain] = res;return res;}public static void main(String[] args) {Solution sol = new Solution();System.out.println(sol.countBalancedPermutations("08143")); // 输出12} }
复杂度分析
-
时间复杂度:O(10×T×M)O(10×T×M),其中 TT 为目标和,MM 为最大奇数位数量。
-
空间复杂度:O(10×T×M)O(10×T×M),用于存储记忆化状态。
总结
-
组合数学:将排列问题转化为数字分配问题。
-
动态规划:通过状态压缩减少计算量。
-
预计算与剪枝:显著提升效率的关键技巧。
拓展思考:如何将问题推广到更多进制或更复杂的平衡条件?欢迎在评论区讨论!
希望这篇文章能帮助你深入理解该问题的解决思路!