#P2199. 第3题-小红的站队挑选
          
                        
                                    
                      
        
              - 
          
          
                      1000ms
            
          
                      Tried: 39
            Accepted: 8
            Difficulty: 8
            
          
          
          
                       所属公司 : 
                              京东
                                
            
                        
              时间 :2024年10月19日
                              
                      
          
 
- 
                        算法标签>二分算法          
 
第3题-小红的站队挑选
题面描述
小塔的集训队共有 n 个人,每个人都有 m 个属性。现在需要从中挑选出 k 个人组成战队。战队的属性计算方式是所选 k 个人对应属性的最大值。战队的战斗力取决于这些属性最大值中的最小值,即 m 个属性的最大值的最小值。目标是选择队员使战队的战斗力最大化,请输出战队的最高战斗力。
思路
求最大值容易想到二分答案,将大于mid的看作1,其他看作0,如果不超过k个数或起来为(1<<m)-1,那么说明当前的mid满足条件
思路讲解
1.二分搜索:
初始化 left = 1 和 right = 1000。 通过二分搜索缩小范围,找到最大的满足条件的 mid 值。
2.掩码表示:
对于每个候选人,我们计算一个掩码 mask,其中每个比特位表示某个属性是否大于等于 mid。 例如,若某个候选人的属性值为 [4, 4, 1, 1],对于 mid = 4,则掩码为 0b0011(表示第1和第2个属性大于等于4)。
3.位掩码动态规划:
我们定义 dp[mask],表示覆盖 mask 所需的最少人数。 初始状态下,dp[0] = 0,即不需要任何人即可覆盖空属性。 通过枚举所有掩码组合,计算能够覆盖的最大属性集合并更新最少人数。
4.判断可行性:
如果可以用不超过 k 个人覆盖所有属性,即 dp[(1 << m) - 1] <= k,则当前 mid 可行。
5.更新结果:
如果 mid 可行,记录结果并尝试更大的 mid。 否则,尝试更小的 mid。
时间复杂度分析
时间复杂度分析
1.二分搜索复杂度:由于战斗力范围为1到1000,因此二分搜索的时间复杂度为 O(log(1000))=O(10)。
2.掩码处理复杂度:每次二分查找时,需要遍历所有候选人的属性,计算掩码,并通过位掩码动态规划更新 dp 数组,时间复杂度为 O(n×m+22m)。
3.总体复杂度:总体时间复杂度为 O(T⋅n⋅m ⋅log(1000)),其中 T 是测试数据组数,n 是候选人数,m 是属性个数。由于 m≤10,时间复杂度是可以接受的
cpp
#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9; // 定义一个足够大的常数,用于初始化DP数组
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int T; // 测试数据组数
	cin >> T;
	
	while(T--){
		int n, m, k;
		cin >> n >> m >> k; // 读取候选人数量n,属性数量m,选择人数k
		
		// 读取每个战士的m个属性
		vector<vector<int>> A(n, vector<int>(m));
		for(auto &row: A){
			for(auto &x: row){
				cin >> x;
			}
		}
		
		// 二分查找的范围为属性值的最小值1到最大值1000
		int left = 1, right = 1000, res = 0;
		
		while(left <= right){
			int mid = left + (right - left) / 2; // 取中间值作为当前尝试的战斗力
			
			// 统计每种掩码出现的次数
			// 掩码表示战士覆盖的属性集合
			vector<int> freq(1 << m, 0);
			for(int i = 0; i < n; i++){
				int mask = 0;
				for(int j = 0; j < m; j++) {
					if(A[i][j] >= mid){
						mask |= (1 << j); // 如果第j个属性值 >= mid,则在掩码中置位
					}
				}
				if(mask != 0){
					freq[mask]++; // 统计该掩码出现的次数
				}
			}
			
			// 位掩码动态规划
			// dp[mask] 表示覆盖mask所需的最少人数
			vector<int> dp(1 << m, INF);
			dp[0] = 0; // 覆盖空集需要0人
			
			for(int mask = 0; mask < (1 << m); mask++){
				if(dp[mask] >= k) continue; // 如果当前覆盖mask所需人数已超过k,跳过
				for(int sub_mask = 1; sub_mask < (1 << m); sub_mask++){
					if(freq[sub_mask] == 0) continue; // 如果没有战士对应该子掩码,跳过
					int new_mask = mask | sub_mask; // 合并当前mask和子掩码
					dp[new_mask] = min(dp[new_mask], dp[mask] + 1); // 更新覆盖new_mask所需的最少人数
				}
			}
			
			int full_mask = (1 << m) - 1; // 全部属性的掩码
			if(dp[full_mask] <= k){
				res = mid; // 当前mid可行,尝试更大的值
				left = mid + 1;
			}
			else{
				right = mid - 1; // 当前mid不可行,尝试更小的值
			}
		}
		
		cout << res << "\n"; // 输出最大战斗力
	}
}
python
INF = int(1e9)  # 定义一个足够大的常数,用于初始化DP数组
def main():
    T = int(input())  # 测试数据组数
    
    while T > 0:
        T -= 1
        
        # 读取候选人数量n,属性数量m,选择人数k
        n, m, k = map(int, input().split())
        
        # 读取每个战士的m个属性
        A = [list(map(int, input().split())) for _ in range(n)]
        
        # 二分查找的范围为属性值的最小值1到最大值1000
        left, right, res = 1, 1000, 0
        
        while left <= right:
            mid = (left + right) // 2  # 取中间值作为当前尝试的战斗力
            
            # 统计每种掩码出现的次数
            # 掩码表示战士覆盖的属性集合
            freq = [0] * (1 << m)
            for i in range(n):
                mask = 0
                for j in range(m):
                    if A[i][j] >= mid:
                        mask |= (1 << j)  # 如果第j个属性值 >= mid,则在掩码中置位
                if mask != 0:
                    freq[mask] += 1  # 统计该掩码出现的次数
            
            # 位掩码动态规划
            # dp[mask] 表示覆盖mask所需的最少人数
            dp = [INF] * (1 << m)
            dp[0] = 0  # 覆盖空集需要0人
            
            for mask in range(1 << m):
                if dp[mask] >= k:
                    continue  # 如果当前覆盖mask所需人数已超过k,跳过
                for sub_mask in range(1, 1 << m):
                    if freq[sub_mask] == 0:
                        continue  # 如果没有战士对应该子掩码,跳过
                    new_mask = mask | sub_mask  # 合并当前mask和子掩码
                    dp[new_mask] = min(dp[new_mask], dp[mask] + 1)  # 更新覆盖new_mask所需的最少人数
            
            full_mask = (1 << m) - 1  # 全部属性的掩码
            if dp[full_mask] <= k:
                res = mid  # 当前mid可行,尝试更大的值
                left = mid + 1
            else:
                right = mid - 1  # 当前mid不可行,尝试更小的值
        
        print(res)  # 输出最大战斗力
if __name__ == '__main__':
    main()
java
import java.util.*;
public class Main {
    static final int INF = (int) 1e9; // 定义一个足够大的常数,用于初始化DP数组
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        
        int T = sc.nextInt(); // 测试数据组数
        
        while (T-- > 0) {
            int n = sc.nextInt(); // 读取候选人数量n
            int m = sc.nextInt(); // 读取属性数量m
            int k = sc.nextInt(); // 选择人数k
            
            // 读取每个战士的m个属性
            int[][] A = new int[n][m];
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < m; j++) {
                    A[i][j] = sc.nextInt();
                }
            }
            
            // 二分查找的范围为属性值的最小值1到最大值1000
            int left = 1, right = 1000, res = 0;
            
            while (left <= right) {
                int mid = left + (right - left) / 2; // 取中间值作为当前尝试的战斗力
                
                // 统计每种掩码出现的次数
                // 掩码表示战士覆盖的属性集合
                int[] freq = new int[1 << m];
                for (int i = 0; i < n; i++) {
                    int mask = 0;
                    for (int j = 0; j < m; j++) {
                        if (A[i][j] >= mid) {
                            mask |= (1 << j); // 如果第j个属性值 >= mid,则在掩码中置位
                        }
                    }
                    if (mask != 0) {
                        freq[mask]++; // 统计该掩码出现的次数
                    }
                }
                
                // 位掩码动态规划
                // dp[mask] 表示覆盖mask所需的最少人数
                int[] dp = new int[1 << m];
                Arrays.fill(dp, INF);
                dp[0] = 0; // 覆盖空集需要0人
                
                for (int mask = 0; mask < (1 << m); mask++) {
                    if (dp[mask] >= k) continue; // 如果当前覆盖mask所需人数已超过k,跳过
                    for (int sub_mask = 1; sub_mask < (1 << m); sub_mask++) {
                        if (freq[sub_mask] == 0) continue; // 如果没有战士对应该子掩码,跳过
                        int new_mask = mask | sub_mask; // 合并当前mask和子掩码
                        dp[new_mask] = Math.min(dp[new_mask], dp[mask] + 1); // 更新覆盖new_mask所需的最少人数
                    }
                }
                
                int full_mask = (1 << m) - 1; // 全部属性的掩码
                if (dp[full_mask] <= k) {
                    res = mid; // 当前mid可行,尝试更大的值
                    left = mid + 1;
                } else {
                    right = mid - 1; // 当前mid不可行,尝试更小的值
                }
            }
            
            System.out.println(res); // 输出最大战斗力
        }
    }
}
        题目内容
小红的集训队共有 n 个人,每个人都有 m 个属性,现在需要挑选其中的 k 个人组成战队出战。
战队也有 m 个属性,这些属性的计算方式是,挑选出的 k 个人里该属性的最大值。
由于“木桶理论”(一只木桶能装多少水取决于它最短的那块木板),战队的战斗力,取决于这个战队的 m 个属性里的那个最小值。
问如何挑选队员,可以使得战队的战斗力最强,请输出战队的最高战斗力。
输入描述
每个测试文件均包含多组测试数据。
第一行输入一个整数 T(1≤T≤100),代表数据组数,每组测试数据描述如下:
第一行三个整数 n,m,k(1≤n≤3×105;1≤m≤10;1≤k≤n)。代表候选人量、属性数量、站队需要的总人数。
接下来 n 行,每行输入 m 个整数,a1,a2,...,am(1≤ai≤103)代表每一位战士的 m 项属性。
除此之外,保证全部测试数据的 n×m 之和不超过 3×105。
输出描述
对于每一组测试数据,在一行上输出一个整数,代表候选人能组成的站队的最大总战斗力。
样例1
输入
1
3 5 3
3 9 6 4 6
6 9 3 1 1
8 8 9 3 7
输出
4
样例2
输入
1
5 2 2
1 6
2 6
3 7
4 3 
5 2
输出
5