回溯法-符号三角形问题
算法思想:回溯法
实际问题:符号三角形问题
编写语言:Java
问题描述
如下图是由 14 个 + 和 14 个 - 组成的符号三角形,两个同号的下面是 +,两个异号的下面是 -。
设符号三角形的第一行有 n 个符号,求对于给定的 n,计算有多少个不同的符号三角形,使其所包含的 + 和 - 的数量相同。
解题思路
- 设 + 为1,- 为 0,这样可以使用异或运算(相同为 0,不同为 1)表示符号三角形的关系:
- “+ +” 为 +:1 ^ 1 = 0
- “- -” 为 +:0 ^ 0 = 0
- “+ -” 为 -:1 ^ 0 = 1
- “- +” 为 -:0 ^ 1 = 1。
- 使用 x[n] 表示第一行的总共 n 个符号的取值,并且定义:
- x[i] = 0:第一行第 i 个符号的取值为 -。
- x[i] = 1:第一行第 i 个符号的取值为 +。
- 使用递归回溯,不断改变第一行的每个符号,搜索符合条件的解。
- 剪枝函数(剪枝操作,或者称作递归终止条件)是:当前 + 或者 - 的符号数量大于一半,或者列数达到指定上限(因为解题时是从第一列开始找,一直找到第 n 列截止)
代码解疑
此处可以跳过,直接看代码,如果有不懂的地方,可以返回来看这里。
位操作:从效率上讲,一个移位指令占2个机器周期,而乘除法指令占4个机器周期。故在做乘除法时,使用位运算有着更高的效率。在本次博文写的代码中,主要用到了两个技巧:与运算、异或运算与移位操作。
与运算:与运算是一种逻辑运算技巧,它的规则是:真与真=真、假与真=假、真与假=假、假与假=假;即满足两个条件都为真时,结果才为真。在 Java 中,与运算符是 &,& 表示按位与,并且 0 表示假,1 表示真,即 1 & 1 = 1、0 & 1 = 0、1 & 0 = 0、0 & 0 = 0。若两个数相与,如 4 和 3与为 0,就可以表示为 100 & 011 = 0,在计算机中,十进制的数是用二进制表示的,如 4 在计算机中的表示是 100。如需要了解更详细的知识,请自行百度。
扯远了,回归正题。在代码中,我们有判断一个数的奇偶性。使用了这样的技巧:n & 1,在计算机中,奇数的最低位的值必然为 1(其他高位是2的整数倍,相加必然为偶数,最低位如果是0,就无法构成奇数),而 1 是最小的奇数,除了最低位,其他位都为 0。按照与运算的规则,**任何一个数 n 与上 1,除了最低位,其他位都是 0。如果 n 为奇数,与的结果最低位为 1,结果是 1;如果 n 为偶数,与的结果最低位为 0,结果是 0。**我们因而可以形如 n & 1 的式子来判断一个数的奇偶性。
异或运算:逻辑运算的一种,前面解释过,此处不讲了。
移位操作:在 Java 中,移位运算符使用 »(右移)和 «(左移)表示的。如 4 右移一位,可以表示为 4 » 1,从二进制的角度将,100(4) 变成了 010(2),即缩小了一半。如果左移,则是增大一倍,在有大量乘除操作的场景中,使用移位操作可以极大的提升效率。
剪枝操作的使用位置有两处,二选一,但应注意两个位置的判断逻辑有差别。具体的代码注释中有详细说明。
根据符号三角形的构成规则,如果三角形第一行有 n 列,那么符号三角形就有 n 行,即行数和列数相等。并且相邻两行之间的列数差 1,带入等差数列的公式,可以得出符号三角形的符号总数为 n * (n + 1) / 2。
因为 + 和 - 的数量必须相等,所以符号三角形的符号数量是偶数,即 n * (n + 1) / 2 是偶数
统计完一种构造方式后,需要还原已统计符号的数量,才能进行下一次的符号三角形构造。
Java 代码
public class SymbolicTriangleProblem {
// 第一行的符号个数
public static int n;
// half 的目标数值为 (n * (n + 1) / 2) / 2,即 n * (n + 1) / 4
public static int half;
// 当前"-"的个数 c:count
public static int c;
// 符号三角形矩阵
public static int[][] p;
// 符号数组 s:sign
public static String[] s = {"+", "-"};
// 已找到的符号三角形的个数
public static long sum;
public static void main(String[] args) {
compute(4);
System.out.println("符合条件的符号三角形总数: " + sum);
}
/**
* 计算指定列数下的符号三角形
*
* @param col 符号三角形第一行的列数,
* 其必须满足 (col * (col + 1) >> 1) 是偶数,
*/
public static void compute(int col) {
n = col;
c = 0;
sum = 0;
// >> 右移一位,表示当前数除以 2
// 当前的符号总个数,为 1 + 2 + ... + n 的数量
half = n * (n + 1) >> 1;
// 符号数量为单数,返回
if((half & 1) == 1) {
return;
}
half >>= 1;
p = new int[n+1][n+1];
// 从第1行的第1个列开始查找
backtrack(1);
}
/**
* 回溯函数
*
* @param col 当前正在查找的位置:第 col 列
*/
public static void backtrack(int col) {
/**
* 如果将剪枝操作移动到这里,总数就不能算
* (col * (col + 1) >> 1,而应该算 (col * (col - 1) >> 1,
* 即算少一列的情况,因为可能会出现 col > n 的情况,导致结果出错
*/
/*
if(c > half || ((col * (col - 1) >> 1) - c > half)) {
return;
} */
// 当前的列数超过规定的列数
if(col > n) {
sum++;
// 打印符号
System.out.println("第 " + sum + " 个符号三角形:");
for(int i = 1; i <= n; i++) {
// 打印符号三角形中每一行开始的空白
for(int j = 1; j < i; j++) {
System.out.print(" ");
}
for(int j = 1; j <= (n - i + 1); j++) {
System.out.print(s[p[i][j]] + " ");
}
System.out.println();
}
System.out.println();
} else {
// 将"+"设为0,"-"设为1
for(int i = 0; i < 2; i++) {
p[1][col] = i;
// 统计"-"的个数
c += i;
// 判断接下来的 n - 1 行
// 符号三角形问题中,行的总数等于列的总数
for(int j = 2; j <= col; j++) {
// 通过异或运算求其余行数的放置方式
p[j][col - j + 1] =
p[j - 1][col - j + 1] ^ p[j - 1][col - j + 2];
c += p[j][col - j + 1];
}
// + 或者 - 的数量未超过一半
// 此处使用 col * (col + 1) >> 1,是因为不会出现 col > n的情况
if(c <= half && ((col * (col + 1) >> 1) - c <= half)) {
backtrack(col + 1);
}
// 还原 - 的数量
for(int j = 2; j <= col; j++) {
c -= p[j][col - j + 1];
}
c -= i;
}
}
}
}
运行结果
下图为列数为 4 的结果: