T1
题目描述
有n中不同口味的水果,第i种水果的数量为a[i],现在需要把水果分给m个人。分给每个人水果的数量必须是相等的,并且每个人只能选择一种水果。也就是说,可以把一种水果分给多个人,但是一个人的水果不能有多种。每个人最多能分到几个水果?
输入描述
输入第一行有一个整数T,代表接下来有T组测试数据。
接下来T组,每组第一行有两个整数n,m,第二行有n个整数a[i],表示第i种水果的个数。
1 <= m <= n <=1000000
1 <= a[i] <= 10^9
保证一个文件内n的总和不超过10^6
输出描述
对于每组数据输出一行,一个整数:每个人最多分到水果的数量。
示例1
输入
2
3 3
2 3 4
2 5
2 4
输出
2
1
题解
前提,需要将水果arr按数量降序排列。
动态规划,
f
(
m
)
=
m
a
x
{
g
(
m
−
1
,
i
)
∣
0
⩽
i
⩽
m
a
x
I
n
d
e
x
+
1
(
或
者
n
)
}
f(m) = max\{ g(m-1,i) | 0\leqslant i\leqslant maxIndex+1(或者n)\}
f(m)=max{g(m−1,i)∣0⩽i⩽maxIndex+1(或者n)}
解释:f(m)代表m个人,每人最多分到的水果数。将此时的分水果方法记到数组count中,count[i]代表第i种水果将分给平均分给count[i]人。
第m人考虑选择那种水果的时候,前m-1个人选择水果的策略不会发生改变,可以用反证法证明。
g
(
m
−
1
,
i
)
g(m-1,i)
g(m−1,i)代表在
m
−
1
m-1
m−1个人的选择方案下,第m个人选i水果时,平均每人分到的水果数。即(int) arr[i] / (count[i] + 1)
:第i种水果总数 / (原人数+第m个人)
遍历i,得到最大的平均分到水果数,即是f(m)。
这里f(m)不必与f(m-1)比较。代码结束。
细节
maxIndex记录已选水果中位置最大的一个。如果第m人选第i种水果,
如果i是从未选择过的水果,即i>maxIndex,为了保证平均最大,而水果已经由大到小排好了,所以我们的策略只会选未选择里面最大的,即i=maxIndex+1。多嘴说一句,arr[maxIndex+1]大于f(m-1),即新选择的水果数量大于m-1人时人均最多水果数量的情况是不存在的,反证法看看,如果存在这种情况,那么m-1的策略f(m-1)一定不是最优策略。不过这句话不影响解题。
否则,其余情况是[0,maxIndex]中选i。
还需注意maxindex+1是否越界
然后比较这些方案,如果选择i=maxIndex+1,则maxIndex更新为maxIndex+1,且count[maxIndex(新的)]=1;
其他i的情况,只需要count[i] += 1;
例如上图,当m=6时,将调取m=5时的策略,即实方块的样式,第一种水果由三个人分,第2种水果由2个人分,count[0]=3; count[1]=2;count[2]=0; ...
,当新加入第6个人时,考虑这个人将选哪一种水果,如虚方块的列举,可以选第0种、第1种、第2种。比较后选择最优策略,更新策略,进入m=7……
复杂度分析
排序log(n),动态规划最复杂的情况是m^2
这个方法的复杂度是
O
(
l
o
g
(
n
)
+
m
2
)
\mathcal{O}(log(n)+m^{2})
O(log(n)+m2)。
import java.util.Comparator;
import java.util.Scanner;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt(); // T组测试数据
while (T > 0) {
T--;
int n = sc.nextInt(); // n种水果
int m = sc.nextInt(); // m个人
Integer[] arr = new Integer[n]; // 记录每种水果有多少个
int[] count = new int[n]; // 记录每种水果有多少人选
for (int i = 0; i < n; i++) { //
arr[i] = sc.nextInt();
}
Arrays.sort(arr, new MyComparator()); // 将水果的从大到小排列
// 如果只有一个人,那他要最大的水果即可
if (m == 1) {
System.out.println(arr[0]);
continue;
}
// 否则记下1个人的情况,用于2个人的取法
int max = arr[0];
count[0] = 1;
int maxIndex = 0; // 递增的水果序列中,必然是从前往后拿的,maxIndex记录了最后取到的水果位置。
// 动态规划,f(m) = max(g(m-1,i) | 0<=i<= maxIndex+1(或者n))
for (int i = 1; i < m; i++) {
int maxval = 0;
int maxi = 0;
int tmpIndex = maxIndex+1<n ? maxIndex+1 : maxIndex; // 处理越界情况,如果越界了,就没有新的水果种类了
for (int j = 0; j <= tmpIndex; j++) {
// 当前这个人i选j种水果可分水果的数量
int cur = arr[j] / (count[j] + 1);
if (cur > maxval) {
maxval = cur;
maxi = j;
}
}
// maxi是第i个人选定的水果种类
// 如果是全新的水果,已有的水果种类指针后移一位,
// 注意这种策略下,选择过的水果必然是连续的。
if (maxi == maxIndex + 1) {
maxIndex += 1;
}
// 记录下第i个人的数据
max = maxval;
count[maxi] += 1;
}
System.out.println(max);
}
}
}
//实现Comparator接口
class MyComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
// 如果o1小于o2,我们就返回正值,如果o1大于o2我们就返回负值,
// 这样颠倒一下,就可以实现降序排序了,反之即可自定义升序排序了
return o2 - o1;
}
}
T2
题目描述
有n个牛牛一起去朋友家吃糖果,第i个牛牛一定要吃ai块糖果。
而朋友家一共只有m块糖果,可能不会满足所有的牛牛都吃上糖果。
同时牛牛们有k个约定,每一个约定为一个牛牛的编号对(i, j),表示第i个和第j个牛牛是好朋友,他俩要么一起都吃到糖果,要么一起都不吃。
保证每个牛牛最多只出现在一个编号对中。
您可以安排让一些牛牛吃糖果,一些牛牛不吃。
要求是能吃上糖果的牛牛数量最多(吃掉的糖果总量要小于等于m),并要满足不违反牛牛们的k个约定。
输入描述
第一行2个正整数n、m,1<= n <= 1 0 3 10^{3} 103 , 1<= m <= 10^3。
第二行n个正整数a1,a2,……,an,1<= ai <= 10^6
第三行1个正整数k,0 <= k <= n/2
接下来k行,每行两个正整数i,j,表示第i个牛牛与第j个牛牛有约定。
输出描述
一行一个数字表示最多能吃上糖果的牛牛个数
示例
输入
3 10
5 1 5
1
1 3
输出
2
题解
1- 先使用并查集将牛牛分成若干组
2- 对这些组做动态规划
细节
1- 分组时用到并查集,是一种树的数据结构,并查集是什么东东?请点这里
并查集分类主要有三个函数:
init()
、findRoot()
、merge()
减少复杂度的技巧:路径压缩、按秩合并
参考链接里解释得非常清楚了。
在分组时我们需要提取一些关键信息:
List arrSuger:i的位置放置–>第i组所需的糖果总数
List arrPeople:i的位置放置–>第i组一共有多少人
2- 动态规划
现在arrSuger.size()代表一共有这么多组牛牛(互不相交),
arrSuger的值代表每一组需要的糖果数,arrPeople代表每一组有多少人
使用动态规划,求能吃上糖果的牛牛数量最多的解
定义:
f
(
g
,
s
)
f(g,s)
f(g,s): 前g组,吃不多于糖果数s的最多人数
p
e
o
p
l
e
(
g
)
people(g)
people(g): 第g组的人数
s
u
g
e
r
(
g
)
suger(g)
suger(g): 第g组所需的糖果数
思想是分类讨论:
第g组吃糖果的情况下,有多少牛牛吃上糖果:
p
e
o
p
l
e
(
g
)
+
f
(
g
−
1
,
s
−
s
u
g
e
r
(
g
)
)
people(g) + f(g-1, s - suger(g))
people(g)+f(g−1,s−suger(g))
第g组不吃糖果的情况下,有多少牛牛吃上糖果:
f
(
g
−
1
,
s
)
f(g-1, s)
f(g−1,s)
f
(
g
,
s
)
=
m
a
x
{
f
(
g
−
1
,
s
−
s
u
g
e
r
(
g
)
)
,
f
(
g
−
1
,
s
)
}
f(g,s) = max\{f(g-1, s-suger(g)), f(g-1,s)\}
f(g,s)=max{f(g−1,s−suger(g)),f(g−1,s)}
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
// 1- 使用并查集将牛牛分成若干组
// 2- 再使用动态规划求解
private static int n = 0;
private static int[] parent = new int[0]; // 父节点是谁
private static int[] suger = new int[0]; // suger[i]代表以位置i为根节点的树一共需要多少糖
private static int[] people = new int[0]; // people[i]代表以位置i为根节点的树一共有多少人
private static int[] rank = new int[0]; // 秩,减少树的深度
public static int findRoot(int x) {
// 找到位置i的根节点
if (x == parent[x]) {
return x;
} else {
// 路径压缩,将沿途的父节点都指向根节点,减少下次运行的时间
parent[x] = findRoot(parent[x]);
return parent[x];
}
}
public static void merge(int x, int y) {
// 按秩合并
// 前提是x,y已经是自己的父节点了。
if (rank[x] == rank[y]) {
// 随便指,x --> y
parent[x] = y;
rank[y] += 1;
suger[y] += suger[x];
people[y] += people[x];
} else if (rank[x] < rank[y]) {
// x --> y
parent[x] = y;
suger[y] += suger[x];
people[y] += people[x];
} else { // rank[x] > rank[y]
// y --> x
parent[y] = x;
suger[x] += suger[y];
people[x] += people[y];
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt(); // n个牛牛
int m = sc.nextInt(); // m个糖果
parent = new int[n];
suger = new int[n];
rank = new int[n];
people = new int[n];
// 初始化,自己做自己的父节点,suger[i]代表以位置i为根节点的树一共需要多少糖
for (int i = 0; i < n; i++) {
suger[i] = sc.nextInt();
people[i] = 1;
parent[i] = i;
rank[i] = 1;
}
int k = sc.nextInt(); // k个约定
for (int i = 0; i < k; i++) {
int preRoot = findRoot(sc.nextInt() - 1); // 前一个的根
int nextRoot = findRoot(sc.nextInt() - 1); // 后一个的根
merge(preRoot, nextRoot);
}
// 已经使用并查集将牛牛分类了,现在遍历一遍,如果某个牛牛i是自己的根节点,就取出来。
// 实际上这样的牛牛是一个组的根(代表),
// 它的suger代表它所在的组的所需糖果总数, people代表这组有多少人
ArrayList<Integer> arrSuger = new ArrayList<>();
ArrayList<Integer> arrPeople = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (i == findRoot(i)) {
arrSuger.add(suger[i]);
arrPeople.add(people[i]);
}
}
// 现在arrSuger.size()代表一共有这么多组牛牛(互不相交),
// arrSuger的值代表每一组需要的糖果数,arrPeople代表每一组有多少人
// 下面使用动态规划,求能吃上糖果的牛牛数量最多的最优解
int len = arrSuger.size();
// 初始化
int[][] arr = new int[len + 1][m + 1];
for (int i = 0; i < m + 1; i++) {
arr[0][i] = 0;
}
for (int i = 1; i < len + 1; i++) {
arr[i][0] = 0;
}
// 开始规划
for (int i = 1; i <= len; i++) {
for (int j = 1; j <= m; j++) {
arr[i][j] = Math.max(arrPeople.get(i - 1) + arr[i - 1][Math.max(0, j - arrSuger.get(i - 1))],
arr[i - 1][j]);
}
}
System.out.println(arr[len][m]);
}
}