您当前的位置:首页 > 计算机 > 编程开发 > 编程箴言

计算二维数组的所有组合

时间:02-06来源:作者:点击数:

之前有一篇《一道有趣的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陕西]
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门