[题解] 最小化熟悉程度和 | 豆包MarsCode AI刷题

195 阅读5分钟

问题描述

最近团队中新来了许多同事,小茗同学所在部门希望通过团建来促进新老成员的沟通交流,增进信任,同时提升团队协作力、执行力和竞争力。

当团建活动规模较大时,参与人数过多,一般会分成若干个小组以便于活动展开。然而,这也导致了不同小组的成员交流过少。为了缓解这个问题,团队提出了分布式团建的方法:将活动分成若干轮,每轮分成多个 3 人小组,每个小组自由支配活动经费单独活动。团队中的成员两两之间的熟悉程度互不相同,为了最大化降低成员之间的陌生程度,分组时需要考虑尽可能将不熟悉的成员匹配在一起,通过团建活动彼此熟络。每个 3 人小组的熟悉程度定义为小组内成员两两之间的熟悉程度之和,分组方案需最小化所有小组的熟悉程度之和。

作为一名算法工程师,小茗同学开始着手解决这个问题,但是遇到了一点小困难,想请你帮忙一起解决。

输入格式

第一行为一个整数 N,表示团队成员人数。 接下来 N 行,每行有 N 个整数 r_{i,j},表示成员 i 与成员 j 的熟悉程度。

输出格式

输出一个整数,表示将团队成员分成多个 3 人小组后,熟悉程度之和的最小值。

输入样例

  • 输入样例 1

3

100 78 97

78 100 55

97 55 100

  • 输入样例 2

6

100 56 19 87 38 61

56 100 70 94 88 94

19 70 100 94 43 95

87 94 94 100 85 11

38 88 43 85 100 94

61 94 95 11 94 100

输出样例

  • 输出样例 1

230

  • 输出样例 2

299

备注

对于样例 2,组队方案为 (1, 3, 5) 和 (2, 4, 6),最小的熟悉程度之和为 (19 + 38 + 43) + (94 + 94 + 11) = 299。

数据范围

数据保证 N 是 3 的倍数, r_{i,j} = r_{j,i}, r_{i,i} = 100。

100% 数据:3 ≤ N ≤ 21, 0 ≤ r_{i,j} ≤ 100 。

代码

完全由豆包 marscode ai 给出,我们接下来分析一下代码的实现思路。

import java.util.Arrays;

public class Main {
    public static int solution(int N, int[][] familiar_matrix) {
        int totalMembers = 1 << N; // 总状态数
        int[] dp = new int[totalMembers];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0; // 初始状态

        for (int mask = 0; mask < totalMembers; mask++) {
            if (dp[mask] == Integer.MAX_VALUE) continue; // 无效状态

            // 找到未分组的成员
            for (int i = 0; i < N; i++) {
                if ((mask & (1 << i)) == 0) { // 成员i未分组
                    for (int j = i + 1; j < N; j++) {
                        if ((mask & (1 << j)) == 0) { // 成员j未分组
                            for (int k = j + 1; k < N; k++) {
                                if ((mask & (1 << k)) == 0) { // 成员k未分组
                                    int newMask = mask | (1 << i) | (1 << j) | (1 << k);
                                    int groupSum = familiar_matrix[i][j] + familiar_matrix[i][k] + familiar_matrix[j][k];
                                    dp[newMask] = Math.min(dp[newMask], dp[mask] + groupSum);
                                }
                            }
                        }
                    }
                }
            }
        }

        return dp[totalMembers - 1]; // 所有成员都被分组的状态
    }

    public static void main(String[] args) {
        // 测试用例
        int[][] familiar_matrix1 = {{100, 78, 97}, {78, 100, 55}, {97, 55, 100}};
        int[][] familiar_matrix2 = {
                {100, 56, 19, 87, 38, 61},
                {56, 100, 70, 94, 88, 94},
                {19, 70, 100, 94, 43, 95},
                {87, 94, 94, 100, 85, 11},
                {38, 88, 43, 85, 100, 94},
                {61, 94, 95, 11, 94, 100}};

        System.out.println(solution(3, familiar_matrix1) == 230);
        System.out.println(solution(6, familiar_matrix2) == 299);
    }
}

分析

这段分析是纯zc同学人工分析的,并不是无脑复制ai的话。

我们以用例2为背景来分析代码的实现思路。

题目首先给出了N,N是公司团建的人数,保证是3的倍数。代码中用到了位运算的思想,可以理解为,一个人就是一位,0-代表该人还没有被分入任何小组,1-代表该人已经被分入某个小组。

一共N位的话,每位只可能是0和1,也就一共有 2N2^N种状态。

接下来,定义dp数组含义:dp[mask] = x 表示: 状态为mask 时, 熟悉程度和的最小值是 x.

然后,我描述一下算法过程:

遍历状态

for (int mask = 0; mask < totalMembers; mask++)

但是会过滤掉无效状态, 如 001100 因为题目规定必须是三个人一组, 以 用例2 为例, N = 0, 初始状态 000000, 第一轮循环过后, 所有下标为 “由3个0和3个1” 的组合 的dp数组全部被赋值(不再是MAX_VALUE)

if (dp[mask] == Integer.MAX_VALUE) continue; // 无效状态

接下来, 3重for循环, 试图把1 插入到0的位置, 即为还没有小组的人分配小组. 一个 & 是按位与, 一个 | 是 按位或, 两个< 即 << 是 左移.

for (int i = 0; i < N; i++) {
    if ((mask & (1 << i)) == 0) { // 成员i未分组
        for (int j = i + 1; j < N; j++) {
            if ((mask & (1 << j)) == 0) { // 成员j未分组
                for (int k = j + 1; k < N; k++) {
                    // ...
                }
        }
}

得到新状态

int newMask = mask | (1 << i) | (1 << j) | (1 << k);

得到当前分组的值

int groupSum = familiar_matrix[i][j] + familiar_matrix[i][k] + familiar_matrix[j][k];

取最小值, 因为 111111 可能由 110001 & 001110 得到, 也可能由 111000 & 000111得到, 正好对应题目场景

dp[newMask] = Math.min(dp[newMask], dp[mask] + groupSum);

最终返回全1的状态即可

return dp[totalMembers - 1]; // 所有成员都被分组的状态

总结

  • 一个是复习一下位运算, 一个是能想到用位运算. 这个场景 用 表示 状态 恰好合适
  • 跳过无效状态, 也算是代码中的细节之处了.