问题及分析:
一种平衡的0-1矩阵
考虑n
*n
矩阵的赋值问题:只能赋0和1,n
为偶数,使每一行和列均含n
/2个0及n
/2个1。例如,当n
=4时,两种可能的方案是:
+ - - - - + + - - - - +
| 0 1 0 1 | | 0 0 1 1
|
| 1 0 1 0 | | 0 0 1 1 |
| 0 1 0 1 | | 1 1 0 0
|
| 1 0 1 0 | | 1 1 0 0 |
+ - - - - + + - - - - +
问:对于给定n
,共有多少种不同的赋值方案。
至少有三种可能的算法来解决这一问题:穷举法(brute force)、回溯法(backtracking)及动态规划(dynamic
programming)。穷举法列举所有赋值方案,并逐一找出满足平衡条件的方案。由于共有C(n
,
n
/2)^n
种方案(在一行中,含n/2个0及n/2个1的组合数为C(n,n/2),相当于从n个位置中选取n/2个位置置0,剩下的自然是1
),当n
=6时,穷举法就已经几乎不可行了。回溯法先将矩阵中部分元素置为0或1,然后检查每一行和列中未被赋值的元素并赋值,使其满足每一行和列中0和1的数量均为n
/2。回溯法比穷举法更加巧妙一些,但仍需遍历所有解才能确定解的数目,可以看到,当n
=8时,该题解的数目已经高达116963796250。动态规划则无需遍历所有解便可确定解的数目(意思是划分子问题后,可有效避免若干子问题的重复计算
)。
通过动态规划求解该问题出乎意料的简单。考虑每一行恰含n
/2个0和n
/2个1的k
*n
(1<=k
<=n
)的子矩阵,函数f根据每一行的可能的赋值映射为一个向量,每个向量由n
个整数对构成。向量每一列对应的一个整数对中的两个整数分别表示该列上该行以下已经放置的0和1的数量。该问题即转化为寻找f((n
/2,n
/2),(n
/2,n
/2),...,(n
/2,n
/2))(具有n
个参数或者说是一个含n
个元素的向量)的值。其子问题的构造过程如下:
1) 最上面一行(第k行
)具有C(n
, n
/2)种赋值;
2) 根据最上面一行中每一列的赋值情况(为0或1),将其对应整数对中相应的元素值减1;
3) 如果任一整数对中的任一元素为负,则该赋值非法,不能成为正确解;
4)
否则,完成对k
*n
的子矩阵中最上面一行的赋值,取k
=k
-1,计算剩余的(k
-1)*n
的子矩阵的赋值;
5) 基本情况是一个1*n
的细小的子问题,此时,该子问题的解的数量为0或1,取决于其向量是否是n
/2个(0,
1)和n
/2个(1, 0)的排列。
例如,在上面给出的两种方案中,向量序列为:
((2, 2) (2, 2) (2, 2) (2, 2)) ((2, 2) (2, 2) (2, 2) (2, 2)) k =
4
0 1 0 1 0 0 1 1
((1, 2) (2, 1) (1, 2) (2, 1)) ((1, 2) (1, 2) (2, 1) (2, 1)) k =
3
1 0 1 0 0 0 1 1
((1, 1) (1, 1) (1, 1) (1, 1)) ((0, 2) (0, 2) (2, 0) (2, 0)) k =
2
0 1 0 1 1 1 0 0
((0, 1) (1, 0) (0, 1) (1, 0)) ((0, 1) (0, 1) (1, 0) (1, 0)) k =
1
1 0 1 0 1 1 0 0
((0, 0) (0, 0) (0, 0) (0, 0)) ((0, 0) (0, 0), (0, 0) (0, 0))
动态规划在此的意义在于避免了相同f的重复计算,更进一步的,上面着色的两个f,虽然对应向量不同,但f的值是相同的,想想为什么吧
:D。
该问题解的数量(序列a058527在OEIS)是1, 2, 90, 297200, 116963796250, 6736218287430460752,
...
实现:
import java.util.HashMap;
import java.util.Map;
public class DynamicPrograming {
private static Map<String, Long> map = new HashMap<String, Long>();
private static String acceptKey = null;
public static void main(String[] args) {
long result = DynamicPrograming.f(10);
System.out.println(result);
}
private static void init(int n) {
if(n % 2 != 0) {
System.err.println("n必须为2的倍数");
System.exit(0);
}
acceptKey = "";
for (int i = 0; i < n; i++) {
acceptKey += "00";
}
}
public static long f(int n) {
init(n);
int[][] arr = new int[n][2];
for (int i = 0; i < n; i++) {
arr[i][0] = n / 2;
arr[i][1] = n / 2;
}
return y(arr, n);
}
public static long y(int[][] arr, int n) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
if (arr[i][j] < 0) {
return 0;
}
}
}
String key = calculateKey(arr);
if (key.contains("-1")) {
return 0;
} else if (key.equals(acceptKey)) {
return 1;
}
long result = 0;
if (map.get(key) == null) {
int[] solution = new int[n];
for (int i = 0; i < n / 2; i++) {
solution[i] = 1;
}
int[][] arrCopy = buildParam(solution, arr);
result += y(arrCopy, n);
boolean isFinished = false;
while (true) {
for (int i = n / 2; i < n; i++) {
if (solution[i] != 1) {
isFinished = false;
break;
}
isFinished = true;
}
if (isFinished) {
map.put(calculateKey(arr), result);
return result;
}
// 遍历每行所有的可能结果
// 从右往左找到第一个可以往右移动的1,移动一个位移,记录下标,再从该下标起左往右扫描,将所有1紧挨着排列
for (int i = n - 2; i >= 0; i--) {
if (solution[i] == 1 && solution[i + 1] == 0) {
solution[i] = 0;
solution[i + 1] = 1;
int x = i + 1;
for (int j = x + 1; j < n; j++) {
if (solution[j] == 1) {
solution[j] = 0;
solution[x + 1] = 1;
x++;
}
}
break;
}
}
// 计算下一行函数的参数
arrCopy = buildParam(solution, arr);
result += y(arrCopy, n);
}
} else {
return map.get(key);
}
}
private static int[][] buildParam(int[] solution, int[][] arr) {
int[][] arrRet = arr.clone();
for (int i = 0; i < solution.length; i++) {
arrRet[i] = arr[i].clone();
if (solution[i] == 0) {
arrRet[i][0] -= 1;
} else {
arrRet[i][1] -= 1;
}
}
return arrRet;
}
private static String calculateKey(int[][] arr) {
int[][] arrcopy = new int[arr.length][2];
if (arr[0][0] > arr[1][0]
|| (arr[0][0] == arr[1][0] && arr[0][1] >= arr[1][1])) {
arrcopy[0] = arr[0].clone();
arrcopy[1] = arr[1].clone();
} else {
arrcopy[0] = arr[1].clone();
arrcopy[1] = arr[0].clone();
}
int n = 1;
for (int i = 2; i < arr.length; i++) {
if (arr[i][0] < arrcopy[n][0]
|| (arr[i][0] == arrcopy[n][0] && arr[i][1] <= arrcopy[n][1])) {
arrcopy[++n] = arr[i].clone();
continue;
}
for (int j = 0; j <= n; j++) {
if (arr[i][0] > arrcopy[j][0]
|| (arr[i][0] == arrcopy[j][0] && arr[i][1] >= arrcopy[j][1])) {
for (int k = n; k >= j; k--) {
arrcopy[k + 1] = arrcopy[k];
}
arrcopy[j] = arr[i].clone();
n++;
break;
}
}
}
String result = "";
for (int i = 0; i < arrcopy.length; i++) {
for (int j = 0; j < arrcopy[i].length; j++) {
result = result + arrcopy[i][j];
}
}
return result;
}
}
动态规划重在问题分析,将问题划分为子问题直到可以容易的求解,并添加缓存避免重复计算。
分享到:
相关推荐
在这个“动态规划算法学习十例之七”的主题中,我们将聚焦于一个具体的动态规划问题——最长公共子序列(Longest Common Subsequence,简称LCS)。这个问题在计算机科学中具有很高的实用价值,尤其是在比较和分析...
在这个“动态规划算法学习十例之九”的主题中,我们将聚焦于如何通过DP来解决实际问题。尽管描述部分没有提供具体的实例,但从标题来看,我们可以推测这是一个关于动态规划应用的系列教程的第九个例子。 动态规划的...
在这个主题“动态规划算法学习十例之六”中,我们将探讨如何利用动态规划方法来解决实际问题。博文链接虽然未提供具体内容,但我们可以根据提供的文件名推测讨论的是一个具体的编程实例。 `Main.java`通常是一个...
标题中的“动态规划算法学习十例之五”表明这篇内容主要关注的是计算机科学中的动态规划算法,这是一种在解决复杂问题时非常有效的优化方法。动态规划通常用于处理具有重叠子问题和最优子结构的问题,通过将大问题...
在这个“动态规划算法学习十例之四”的主题中,我们将专注于背包问题的解决方案。背包问题是一个经典的计算机科学问题,它通常涉及在给定容量的背包中选择物品以最大化总价值。 首先,我们来了解动态规划的基本思想...
在这个“动态规划算法学习十例之一”的主题中,我们将会探讨动态规划的基本概念和一个具体的实例,通过分析`Test.java`源码来深入理解。 首先,动态规划的核心思想是将一个大问题分解为相互重叠的小问题,并通过...
"动态规划算法的应用" 动态规划算法是一种非常强大且广泛应用的算法思想,它可以解决许多复杂的问题。动态规划算法的核心思想是将问题分解...在未来的学习和工作中,我们将继续掌握和应用动态规划算法来解决实际问题。
动态规划算法学习心得(leetcode) 动态规划算法的实质是更好的优化穷举算法,即把算过的子树记录下来减少计算次数。 储存计算过的子树的数列就是dp数列 使用动态规划有三个条件 1.最值问题 一般动态规划的使用场景是...
【达摩老生出品,必属...资源名:matlab实现动态规划算法 程序源码.zip 资源类型:程序源代码 源码说明: 基于matlab实现动态规划的程序,包含完整源码和注释,非常适合借鉴学习 适合人群:新手及有一定经验的开发人员
学习动态规划算法
AOVAOE网络 动态规划算法PPT学习教案.pptx
"学习电脑信息五大常用算法之二:动态规划算法" 动态规划算法是五大常用算法之一,是解决多阶段决策问题的有效方法。它的基本思想是将问题分解为多个阶段,每个阶段都有其状态和决策,然后通过决策序列来解决问题。...
本资源“动态规划算法介绍”旨在为ACM竞赛和算法初学者提供一个理解和应用动态规划的良好起点,帮助扩展解题思路。 动态规划的核心思想是将复杂问题分解为多个子问题,并通过存储和重用子问题的解决方案来避免重复...
动态规划是一种重要的算法思想,广泛应用于解决复杂优化问题。它通过将大问题分解为相互关联的小问题,并存储每个小问题的解,避免了重复计算,从而达到高效求解的目的。在计算机科学,尤其是算法设计中,动态规划是...
标题中的“DP算法即动态规划算法集锦”,暗示我们将探讨一系列动态规划的经典案例,这些案例可能包括但不限于背包问题、最长公共子序列、最短路径问题、字符串匹配等。动态规划能够处理具有重叠子问题和最优子结构的...
标题中的“动态规划算法及其一些例子”意味着我们将探讨动态规划的基本原理,并通过具体的例子来加深理解。动态规划通常有以下三个关键步骤: 1. **定义状态**:明确表示问题的中间阶段,例如,斐波那契数列中的第n...
动态规划算法通常涉及到一个最优决策序列的问题,它通过自底向上的方式求解,将原问题分解为子问题,通过填充一个表格(也称为状态转移表)来逐步构建出最优解。在这个过程中,每个子问题的解都会被记录下来,以便...
这个压缩包文件“动态规划算法(学习算法分析二)”显然包含了一系列与动态规划相关的学习资源,包括源代码和作者的心得体会。下面,我们将深入探讨动态规划的基本概念、应用场景以及相关知识点。 动态规划的核心思想...
总的来说,这个C++实现的0-1背包问题动态规划算法不仅展示了如何利用动态规划解决问题,还提供了代码调试和测试的方法,是学习和理解动态规划算法的一个优秀实例。通过深入研究和实践,我们可以掌握这一重要的算法...
贪心算法和动态规划是两种在计算机科学中广泛使用的算法设计策略,特别是在解决优化问题时。这两种方法在处理复杂问题时各有特点,但都致力于找到全局最优解。 贪心算法是一种局部最优解策略,它每次选择当前状态下...