算法-随机试卷

随机试卷

给定分数列表scoreList,随机数量randomQuantity,随机分数randomScore;要求实现一个校验方法,判断分数列表中是否存在指定数量的题目,总分为randomScore

递归版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**  
 * 生成校验  
 * @param  
 *  
 * @return  
 * <br>-----------------------------------------------------<br>  
 *  
 * @author: mujingya  
 * @date: 2024/4/2 16:25  
 */public static boolean generateCheck1(List<Integer> scoreList, int randomQuantity, int randomScore) {  
    // 分数列表 随机数量 随机分数 从 0位置 0分数开始  
    return generateCheckHelper(scoreList, randomQuantity, randomScore, 0, 0);  
}  
  
  
/**  
 * 辅助校验  
 *  
 * @param scoreList 所有的题目分数列表  
 * @param remainingQuantity 剩余数量  
 * @param randomScore 随机分数  
 * @param start  
 * @param currentScoreSum  
 * @author: mujingya  
 * @date: 2024/4/2 18:36  
 */private static boolean generateCheckHelper(List<Integer> scoreList, int remainingQuantity, int randomScore, int start, int currentScoreSum) {  
    // 剩余题目数量为0时  
    if (remainingQuantity == 0) {  
        // 返回是否相等  
        return currentScoreSum == randomScore;  
    }  
    // start开始  i++   
     for (int i = start; i < scoreList.size(); i++) {  
        // 每次 剩余题目数量-1 start位置+1 分数累加  
        Integer currentScore = scoreList.get(i);  
        // 递归校验  
        boolean recursionRes = generateCheckHelper(scoreList, remainingQuantity - 1, randomScore, i + 1, currentScoreSum + currentScore);  
	        if (recursionRes) {  
	            // 返回true 即递归剩余题目数量为0分数符合 直接返回  
	            return true;  
	        }  
	        // false 继续循环校验 i++    
        }  
    return false;  
}

三维动态规划

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static boolean generateCheck3(List<Integer> scoreList, int randomQuantity, int randomScore) {  
    // 分数列表长度  
    int n = scoreList.size();  
    // dp[i][j][k] 表示从前i个分数中选择k个是否可以组成总和为j  
    boolean[][][] dp = new boolean[n+1][randomScore+1][randomQuantity+1];  
  
    // 初始化  
    for (int i = 0; i <= n; i++) {  
        // 不选择任何分数时,总和为0是可能的  
        dp[i][0][0] = true;  
    }  
  
    // 动态规划填表  
    for (int i = 1; i <= n; i++) {  
        // 当前分数  
        int score = scoreList.get(i-1);  
        // 总分  
        for (int j = 0; j <= randomScore; j++) {  
            // 选择k个 从1开始 至少选择一个分数  
            for (int k = 1; k <= randomQuantity; k++) {  
                // 默认先不选择当前分数  dp继承之前的状态  
                dp[i][j][k] = dp[i-1][j][k];  
                // 总分减去当前分数>=0 即可以选择当前分数(第i个分数)  
                if (j - score >= 0) {  
                    // (参数递减)i-1个分数 是否存在k-1个分数 总和为j-score 存在就true 不存在就继承之前状态  
                    // dp[i][j][k] = dp[i][j][k] || dp[i-1][j-score][k-1];  
                    dp[i][j][k] |= dp[i-1][j-score][k-1];  
                }  
            }  
        }  
    }  
  
    // 返回是否存在一种方式可以选择randomQuantity个分数使总和为randomScore  
    return dp[n][randomScore][randomQuantity];  
}

二维动态规划

思路:假定题目列表为[1, 3 ,4, 2],要求题目数量2个,总分为5分 第一步,初始化dp表,dp[j][k]表示是否存在k个组成总分j,即存在0个题目组成0分

k/j012
0TFF
1FFF
2FFF
3FFF
4FFF
5FFF

第二步,循环分数列表 动态规划填表 (每次循环为在(0,i]范围查找 更新dp表) 不再考虑0的行和列 在[1]查找, 是否存在1个组成1分?T 是否存在1个组成2分? 是否存在1个组成3分? 是否存在1个组成4分? 是否存在1个组成5分?

是否存在2个组成1分? 是否存在2个组成2分? 是否存在2个组成3分? 是否存在2个组成4分? 是否存在2个组成5分?

k/j012
0TFF
1FTF
2FFF
3FFF
4FFF
5FFF

第三步,循环分数列表 动态规划填表 (每次循环为在(0,i]范围查找 更新dp表) 在[1,3]查找, 是否存在1个组成1分?T 是否存在1个组成2分? 是否存在1个组成3分?T 是否存在1个组成4分? 是否存在1个组成5分?

是否存在2个组成1分? 是否存在2个组成2分? 是否存在2个组成3分? 是否存在2个组成4分? T 是否存在2个组成5分?

k/j012
0TFF
1FTF
2FFF
3FTF
4FFT
5FFF

第四步,循环分数列表 动态规划填表 (每次循环为在(0,i]范围查找 更新dp表) 在[1,3,4]查找, 是否存在1个组成1分?T 是否存在1个组成2分? 是否存在1个组成3分?T 是否存在1个组成4分?T 是否存在1个组成5分?

是否存在2个组成1分? 是否存在2个组成2分? 是否存在2个组成3分? 是否存在2个组成4分? T 是否存在2个组成5分? T (这一步就确定了)

k/j012
0TFF
1FTF
2FFF
3FTF
4FTT
5FFT

第五步,循环分数列表 动态规划填表 (每次循环为在(0,i]范围查找 更新dp表) 在[1,3,4,2]查找, 是否存在1个组成1分?T 是否存在1个组成2分?T 是否存在1个组成3分?T 是否存在1个组成4分?T 是否存在1个组成5分?

是否存在2个组成1分? 是否存在2个组成2分? 是否存在2个组成3分? T 是否存在2个组成4分? T 是否存在2个组成5分? T

k/j012
0TFF
1FTF
2FTF
3FTT
4FTT
5FFT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static boolean generateCheck(List<Integer> scoreList, int randomQuantity, int randomScore) {  
    // 题目总数量 小于 随机数量 直接返回false  
    if (scoreList.size() < randomQuantity){  
        return false;  
    }  
    int sum = scoreList.stream().mapToInt(Integer::intValue).sum();  
    // 题目总分数 小于 随机分数 直接返回false  
    if (sum < randomScore){  
        return false;  
    }  
    // 分数列表长度  
    int n = scoreList.size();  
    // dp[j][k] 表示 是否存在k个 组成总分j  
    boolean[][] dp = new boolean[randomScore + 1][randomQuantity + 1];  
  
    dp[0][0] = true;  
    // 循环分数列表 动态规划填表 (每次循环为在(0,i]范围查找 更新dp表)  
    for (int i = 1; i <= n; i++) {  
        // 当前分数  
        int score = scoreList.get(i - 1);  
        // j为总分 从最大开始  
        for (int j = randomScore; j >= score; j--) {  
            // 选择k个 从1开始  
            for (int k = 1; k <= randomQuantity; k++) {  
                // 前i个分数中 是否存在k-1个分数总和为j-score(即依赖上次的)  或  是否存在k个分数 总和为j  
                // dp[j][k] = dp[j][k] || dp[j - score][k - 1];               
                 dp[j][k] |= dp[j - score][k - 1];  
            }  
        }  
    }  
    return dp[randomScore][randomQuantity];  
}

需求实际场景

给定题目列表,随机生成符合条件的(数量和分数)具体题目列表 思路是打乱题目,再获取匹配的第一组

递归版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public static List<QuestionDto> generateRandomQuestions1(List<QuestionDto> allQuestionList, int randomQuantity, int randomScore) {  
    Random random = new Random();  
    // 随机打乱题库 增加随机性  
    Collections.shuffle(allQuestionList, random);  
    List<QuestionDto> result = new ArrayList<>();  
    generateHelper(allQuestionList, randomQuantity, randomScore, 0, 0, new ArrayList<>(), result);  
    return result.isEmpty() ? null : result;  
}  
  
/**  
 * @param allQuestionList 所有的题目列表(题库)  
 * @param remainingQuantity 递归 剩余需要选取的题目数量  
 * @param randomScore 随机分数  
 * @param start 当前递归的起始位置下标 避免重复选取题目 (分别从0,1,2,3...开始)  
 * @param currentScoreSum 当前分数总和  
 * @param currentList  当前 已选取的题目组合列表  
 * @param result  
 *  
 * @return  
 */  
private static boolean generateHelper(List<QuestionDto> allQuestionList, int remainingQuantity, int randomScore, int start, int currentScoreSum, List<QuestionDto> currentList, List<QuestionDto> result) {  
    // 剩余数量为0 这轮组合完毕  
    if (remainingQuantity == 0) {  
        // 判断是否符合分数的条件  
        if (currentScoreSum == randomScore) {  
            // 符合条件 立即返回  
            result.addAll(currentList);  
            return true;        }  
        return false;  
    }  
  
    // 循环问题列表  
    for (int i = start; i < allQuestionList.size(); i++) {  
        // 当前问题  
        QuestionDto question = allQuestionList.get(i);  
        // 当前问题分数  
        int currentScore = Integer.parseInt(question.getScore());  
        if (currentScoreSum + currentScore > randomScore) {  
            // 分数和 超过目标分数 跳过  
            continue;  
        }  
        // 否则 加入本次循环列表  
        currentList.add(question);  
        // 继续 剩余数量-1 循环位置+1 当前分数/列表增加  
        if (generateHelper(allQuestionList, remainingQuantity - 1, randomScore, i + 1, currentScoreSum + currentScore, currentList, result)) {  
            // 剩余数量为0 分数相等 立即返回  
            return true;  
        }  
        // 否则即为 剩余数量为0 分数不相等  
        // 回溯  
        currentList.remove(currentList.size() - 1);  
    }  
  
    return false;  
}

三维动态规划

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public static List<QuestionDto> generateRandomQuestions(List<QuestionDto> allQuestionList, int randomQuantity, int randomScore) {  
    // 打乱  
    Collections.shuffle(allQuestionList, new Random());  
    int n = allQuestionList.size();  
    // i 当前考虑的 allQuestionList 中的索引  
    // k 需要选择的剩余问题数量  
    // j 选择需要达到的剩余分数  
    // 是否有可能从索引 i 开始选择k个问题 总分是j (和校验逻辑一样)  
    boolean[][][] dp = new boolean[n+1][randomQuantity+1][randomScore+1];  
  
    //  k == 0 且 j == 0 不选择任何问题 总是 true    
    for (int i = 0; i <= n; i++) {  
        dp[i][0][0] = true;  
    }  
  
    // 动态规划填表 循环n次(问题数量)  
    for (int i = 1; i <= n; i++) {  
        // 对于每个问题 考虑所有可能的问题数量(k)和分数(j)组合  
        // 当前问题   i-1为真正索引位置  
        QuestionDto questionDto = allQuestionList.get(i - 1);  
        int currentScore = Integer.parseInt(questionDto.getScore());  
        // 问题从1开始  
        for (int k = 1; k <= randomQuantity; k++) {  
            // 是否可以在 包括当前问题 或 不包括当前问题  的情况下  达到特定的问题数量和总分  
            for (int j = 1; j <= randomScore; j++) {  
                // 对于索引为i的每个问题 有两种情况  
                // 1.不选择 即上一层(i-1)是否直接有符合情况的组合(k题j分) dp[i][k][j] = dp[i-1][k][j]。  
                dp[i][k][j] = dp[i-1][k][j];  
                // 2.选择 只有当前问题的分数不超过j时才可能  
                if (j >= currentScore) {  
                    // 如果选择 需要检查在此之前上一层(i-1)是否存在 k-1个分数总和为j-currentScore (即就差现在这一题就是j分了) dp[i][k][j] = dp[i][k][j] || dp[i-1][k-1][j-currentScore]
                                        dp[i][k][j] = dp[i][k][j] || dp[i-1][k-1][j-currentScore];  
                }  
            }  
        }  
  
    }  
  
    if (!dp[n][randomQuantity][randomScore]) {  
        return new ArrayList<>();  
    }  
    List<QuestionDto> result = new ArrayList<>();  
    // 剩余分数  
    int remainingScore = randomScore;  
    // 逆向回溯  
    for (int i = n, k = randomQuantity; i > 0 && k > 0; i--) {  
        // 当前考虑的问题 即位于i-1(真正的索引)的问题 是否真的需要  
        if (dp[i-1][k][remainingScore]) {  
            // 为true 则在没有当前问题的情况下 也能满足  
            // 如[1,3,4,2] 2道题5分 没有最后一个2也能满足  
            continue;  
        }  
        // dp[i-1][k][j] 为假但 dp[i][k][j] 为真  
        // 该问题是 应该是 结果的一部分  
        QuestionDto question = allQuestionList.get(i-1);  
        // 加入结果并减少剩余分数和数量  
        result.add(question);  
        remainingScore -= Integer.parseInt(question.getScore());  
        k--;  
        // 继续循环  
    }  
    return result;  
}
updatedupdated2024-11-082024-11-08