之前有一篇《一道有趣的Java编程题》,后来发现我的解题方式只适合于通过正确答案一定有错误答案的题,如果通过正确答案找不到对应错误答案的话,就解不出来了。通过和同事交流,知道了可以使用暴力破解,这个说起来好像很简单,但是写起来还是挺难的,难点就在于如何把所有的答案组合统计起来,示例如下:
A 1 2
B 3 4
C 5 6
假设有A、B、C三组答案,每一组是两个答案,每组有且仅有一个答案正确,则答案组合的数量有8组(2的3次幂),所有答案组合如下:
1、3、5
1、3、6
1、4、5
1、4、6
2、3、5
2、3、6
2、4、5
2、4、6
这数据还不多,自己口头也能找出来这个所有的组合,但是数据多的话,口头肯定是算不出来的,太累人了,如果通过代码实现呢?这是比较有挑战性的,花了我半天时间才写出来,分析如下:
如上图,这是一个3 x 2的二维数据,这是题目数据,然后我们再看所有答案组合,如下:
这个答案组合也是一个二维数组,我们的难题就是如何把前一个二维数组转换为后一个二维数组,首先要找出规律,如下:
1、题目数组放到结果数组中时,第一行放到第一列,第二行放到第二列,第三行放到第三列,如下:
这里我们发现,左边数组的行索引和右边的列索引一样,即左边数组第一行的数据会放到右边数组的第一列,左边第二行会放到右边第二列,左边第三行会放到右边第三列。
2、每个答案都正好存在4次,如下:
这里可以发现
a、左边第一行放到右边时,每个数字连续放4次,即先放4次1,再放4次2。
b、左边第二行放到右边时,则两个两个交替放,即先往两个3,再放两个4,再放两个3,再放两个4,如果换一种理解可以这样,先放两个3,然后空两个再放两个3,然后往4时也一样,但是放4的起始位置要注意不是从0开始的。
c、左边第三行放到右边时,则是隔一个放一个。
右边数组中,每个数字可以连续放多少次呢?第一、第二、第三列分别为4、2、1,这个数字如何得来?可通过下面的发现规律:
如上图,连续放的次数和行索引组合起来,如下:
0 - 4 2的2次方
1 - 2 2的1次方
2 - 1 2的0次方
这里发现了有0、1、2和2、1、0是反顺序,如何通过0、1、2得到2、1、0呢?因为总共有3行,可以使用变量3来完成,如下:
3 - 1 - 0 = 2
3 - 1 - 1 = 1
3 - 1 - 2 = 0
所以,通过已经行数3和行索引,就能得到2、1、0。
找到了这些规律,我们就可以写代码了,如下:
fun main() {
val allAnswers = arrayOf(
arrayOf(1, 2),
arrayOf(3, 4),
arrayOf(5, 6),
)
val answerSheet = getAnswerSheet(allAnswers)
answerSheet.forEach { println(it.contentToString()) }
}
private fun getAnswerSheet(allAnswers: Array<Array<Int>>): Array<Array<Int>> {
// 计算答案表的大小为:2的allAnswers.size次幂
val sheetSize = 2.0.pow(allAnswers.size).toInt()
// 创建答案表
val answerSheet = Array(sheetSize) { Array(allAnswers.size) { 0 } }
// 遍历所有答案,并填充到答案表中
allAnswers.forEachIndexed { outIndex, answers ->
// 计算答案可以连续填充的最大次数
val maxPutCount = 2.0.pow((allAnswers.size - 1 - outIndex).toDouble()).toInt()
answers.forEachIndexed { index, answer ->
var putCount = 0
for (i in answerSheet.indices) {
if (++putCount <= maxPutCount) {
answerSheet[i + index * maxPutCount][outIndex] = answer
} else if (putCount % maxPutCount == 0) {
putCount = 0
}
}
}
}
return answerSheet
}
运行结果如下:
[1, 3, 5]
[1, 3, 6]
[1, 4, 5]
[1, 4, 6]
[2, 3, 5]
[2, 3, 6]
[2, 4, 5]
[2, 4, 6]
Ok,完美的把所有的组合都打印出来了。学会了这个,似乎也可以玩玩密码的暴力破解了,原理就是把所有的密码数字列出来,然后转换成所有的密码组合,然后每个密码组合都试一下是否为正确密码。
续(2021_04_21):前面说到,每个答案都会填充相同的次数,则循环这个次数即可,之前是循环的次数是比较多的,优化后,如下:
import kotlin.math.pow
fun main() {
val allAnswers = arrayOf(
arrayOf(1, 2),
arrayOf(3, 4),
arrayOf(5, 6),
)
val answerSheet = getAnswerSheet(allAnswers)
answerSheet.forEach { println(it.contentToString()) }
}
private fun getAnswerSheet(allAnswers: Array<Array<Int>>): Array<Array<Int>> {
// 计算答案表的大小为:2的allAnswers.size次幂
val sheetSize = 2.0.pow(allAnswers.size).toInt()
// 创建答案表
val answerSheet = Array(sheetSize) { Array(allAnswers.size) { 0 } }
// 遍历所有答案,并填充到答案表中
allAnswers.forEachIndexed { outIndex, answers ->
// 计算答案可以连续填充的最大次数
val maxPutCount = 2.0.pow((allAnswers.size - 1 - outIndex).toDouble()).toInt()
answers.forEachIndexed { index, answer ->
var putCount = 0
var putIndex = 0
repeat(answerSheet.size / 2) {
answerSheet[putIndex++ + index * maxPutCount][outIndex] = answer
if (++putCount % maxPutCount == 0) {
putIndex += maxPutCount
}
}
}
}
return answerSheet
}
最后,我们把《一道有趣的Java编程题》中的练习题,使用暴力破解实现一下,如下:
import kotlin.math.pow
// 定义一个Answer类来表示一个答案
data class Answer(val number: Int, val name: String) {
override fun toString() = "$number$name"
}
fun main() {
val allAnswers = arrayOf(
arrayOf(Answer(2, "陕西"), Answer(5, "甘肃")),
arrayOf(Answer(2, "湖北"), Answer(4, "山东")),
arrayOf(Answer(1, "山东"), Answer(5, "吉林")),
arrayOf(Answer(3, "湖北"), Answer(4, "吉林")),
arrayOf(Answer(2, "甘肃"), Answer(3, "陕西"))
)
val answerSheet = getAnswerSheet(allAnswers)
val rightAnswer = findRightAnswers(answerSheet)
rightAnswer.forEach { println(it.contentToString()) }
}
private fun getAnswerSheet(allAnswers: Array<Array<Answer>>): Array<Array<Answer>> {
// 计算答案表的大小为:2的allAnswers.size次幂
val sheetSize = 2.0.pow(allAnswers.size).toInt()
// 创建答案表
val answerSheet = Array(sheetSize) { Array(allAnswers.size) { Answer(0, "") } }
// 遍历所有答案,并填充到答案表中
allAnswers.forEachIndexed { outIndex, answers ->
// 计算答案可以连续填充的最大次数
val maxPutCount = 2.0.pow((allAnswers.size - 1 - outIndex).toDouble()).toInt()
answers.forEachIndexed { index, answer ->
var putCount = 0
var putIndex = 0
repeat(answerSheet.size / 2) {
answerSheet[putIndex++ + index * maxPutCount][outIndex] = answer
if (++putCount % maxPutCount == 0) {
putIndex += maxPutCount
}
}
}
}
return answerSheet
}
/** 找到答案表中的所有正确答案 */
fun findRightAnswers(answerSheet: Array<Array<Answer>>): List<Array<Answer>> {
val rightAnswer = mutableListOf<Array<Answer>>()
// 遍历每一组答案,并验证是否正确
answerSheet.forEach { oneGroupAnswer ->
if (isRightAnswer(oneGroupAnswer)) {
rightAnswer.add(oneGroupAnswer)
}
}
return rightAnswer
}
/** 判断一组答案是否正确 */
fun isRightAnswer(oneGroupAnswer: Array<Answer>): Boolean {
val rightAnswers = mutableListOf<Answer>()
oneGroupAnswer.forEach { answer ->
if (rightAnswers.any { it.number == answer.number || it.name == answer.name }) {
return false
}
rightAnswers.add(answer)
}
return true
}
运行结果如下:
[5甘肃, 2湖北, 1山东, 4吉林, 3陕西]