乐团分组问题是一个典型的组合优化问题,要求我们将n位乐手分成尽可能多的乐队,同时满足每位乐手所在乐队的人数必须不少于该乐手的能力值。这个问题在实际生活中也有广泛的应用场景,比如任务分配、资源调度等。
我们有n位乐手,每位乐手i有一个能力值a[i],表示该乐手所在乐队的人数必须≥a[i]。每位乐手只能加入一个乐队。我们的目标是:
如果无法满足所有乐手的分配要求,则返回-1。
以题目给出的示例为例:
输入:
4
2 1 2 1
这表示有4位乐手,他们的能力值分别是2,1,2,1。最优解是分成3个乐队:
这样每个乐队都满足人数≥该乐队中所有乐手的能力值,且乐队数量达到了最大值3。
为了最大化乐队数量,我们需要尽可能多地形成小的乐队。这提示我们可以采用贪心算法:
这种策略的合理性在于:
单纯的贪心策略可能无法处理所有情况,我们需要结合动态规划来确保找到最优解。定义dp[i]为前i个乐手能组成的最大乐队数量。
状态转移方程:
这个状态转移方程的含义是:
对于n ≤ 1e5的规模,这个复杂度是完全可行的。
cpp复制#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll,ll> pii;
const ll p=1e9+7;
const ll N=1e5+10;
int main()
{
ll n;
cin>>n;
vector<ll> v(n+1);
for(ll i=1;i<=n;i++) cin>>v[i];
sort(v.begin()+1,v.end());
vector<ll> dp(n+1);
for(ll i=1;i<=n;i++)
{
if(i-v[i]>=0) dp[i]=max(dp[i-1],dp[i-v[i]]+1);
else dp[i]=dp[i-1];
}
if(n-v[n]>=0) cout<<dp[n-v[n]]+1<<'\n';
else cout<<-1<<'\n';
return 0;
}
cpp复制ll n;
cin>>n;
vector<ll> v(n+1);
for(ll i=1;i<=n;i++) cin>>v[i];
这里我们使用1-based索引来存储乐手的能力值,方便后续处理。
cpp复制sort(v.begin()+1,v.end());
将乐手按能力值从小到大排序,这是贪心策略的基础。
cpp复制vector<ll> dp(n+1);
for(ll i=1;i<=n;i++)
{
if(i-v[i]>=0) dp[i]=max(dp[i-1],dp[i-v[i]]+1);
else dp[i]=dp[i-1];
}
dp数组的填充过程是算法的核心。对于每个乐手i,我们有两种选择:
cpp复制if(n-v[n]>=0) cout<<dp[n-v[n]]+1<<'\n';
else cout<<-1<<'\n';
最后需要检查是否所有乐手都能被分配。如果最大的能力值v[n] > n,显然无法满足,输出-1。否则输出dp[n-v[n]]+1。
我们需要证明先满足能力值小的乐手能够最大化乐队数量。反证法:
假设存在一个最优解,其中某个能力值小的乐手没有被优先满足。那么我们可以调整分组,先满足这个乐手,这样可能会释放更多的乐手给后面的分组,从而可能得到更多的乐队数量。这与假设矛盾。
dp[i]表示前i个乐手的最大乐队数量。我们需要证明状态转移的正确性:
这两种情况覆盖了所有可能性,因此状态转移是正确的。
当前的dp数组使用了O(n)的空间。实际上,我们可以只保留最近需要的dp值,将空间优化到O(1)。不过对于n≤1e5的规模,O(n)的空间已经足够。
这个算法不仅仅适用于乐团分组,还可以应用于:
对于n≤1e5,这个复杂度是完全可行的。
如果n非常大(比如1e7),可以考虑:
纯贪心算法可能无法处理所有情况,比如:
输入:3, [1,1,2]
贪心可能得到2个乐队([1],[1,2]),但最优解是3个乐队([1],[1],[2])
我们的贪心+DP方法可以正确处理这种情况。
纯动态规划可能需要更高的时间复杂度(如O(n^2)),而我们的方法通过结合贪心将复杂度降低到O(nlogn)。
python复制n = int(input())
v = list(map(int, input().split()))
v.sort()
dp = [0] * (n + 1)
for i in range(1, n + 1):
if i >= v[i - 1]:
dp[i] = max(dp[i - 1], dp[i - v[i - 1]] + 1)
else:
dp[i] = dp[i - 1]
if n >= v[-1]:
print(dp[n - v[-1]] + 1)
else:
print(-1)
java复制import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] v = new int[n];
for (int i = 0; i < n; i++) {
v[i] = sc.nextInt();
}
Arrays.sort(v);
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
if (i >= v[i - 1]) {
dp[i] = Math.max(dp[i - 1], dp[i - v[i - 1]] + 1);
} else {
dp[i] = dp[i - 1];
}
}
if (n >= v[n - 1]) {
System.out.println(dp[n - v[n - 1]] + 1);
} else {
System.out.println(-1);
}
}
}
为了加深对这个问题的理解,建议尝试以下类似题目:
这些题目都涉及到贪心算法和优化问题,可以帮助巩固相关概念。
在实际编程比赛中,这类组合优化问题非常常见。我的经验是:
对于这个问题,我最初尝试了纯贪心算法,发现无法处理所有情况。后来通过分析,意识到需要记录中间状态,于是引入了动态规划。这种贪心+DP的组合在解决许多优化问题时都非常有效。