当前位置: 首页 > news >正文

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 的情况。


优化思路:组合数学 + 动态规划

关键观察

  1. 总和奇偶性:若所有数字之和为奇数,直接返回0。

  2. 分配问题:将数字分配到奇数位和偶数位,使得两部分和相等。

数学建模

设总共有 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),用于存储记忆化状态。


总结

  1. 组合数学:将排列问题转化为数字分配问题。

  2. 动态规划:通过状态压缩减少计算量。

  3. 预计算与剪枝:显著提升效率的关键技巧。

拓展思考:如何将问题推广到更多进制或更复杂的平衡条件?欢迎在评论区讨论!


希望这篇文章能帮助你深入理解该问题的解决思路!

相关文章:

  • 力扣:轮转数组
  • Python字典:数据操作的核心容器
  • .Net HttpClient 概述
  • C++线程库
  • 记录一下学习kafka的使用以及思路
  • 黄金、碳排放期货市场API接口文档
  • AI日报 · 2025年5月09日|OpenAI Deep Research 上线 GitHub Connector Beta
  • 【相机标定】OpenCV 相机标定中的重投影误差与角点三维坐标计算详解
  • 【论文阅读】——Articulate AnyMesh: Open-Vocabulary 3D Articulated Objects Modeling
  • Python 基础语法与数据类型(六) - 条件语句、循环、循环控制
  • 全球实物文件粉碎服务市场洞察:合规驱动下的安全经济与绿色转型
  • Flink之Table API
  • U9C对接飞书审批流完整过程
  • 【软件测试】测试用例的设计方法
  • 深入理解 Istio 的工作原理 v1.26.0
  • 【LangChain高级系列】LangGraph第一课
  • 【强化学习】动态规划(Dynamic Programming, DP)算法
  • 从Dockerfile 构建docker镜像——保姆级教程
  • Docker容器启动失败?无法启动?
  • Day 15 训练
  • 19岁女生注射头孢离世后续:院方道歉,医生停职,监管介入
  • 数说母亲节|妈妈的妈妈带娃比例提升,托举效果如何?
  • 中国海警依法驱离日非法进入我钓鱼岛领海船只
  • 商务部:中方愿同各国一道加强合作,促进跨境电商健康可持续发展
  • 明天起,沪苏湖高铁、杭温高铁推出13款新型票制产品
  • 8大类1000多支,中国红十字会已建成10万人规模救援队伍