阿里暑期实习算法岗笔试题(java)2020.04.29场

2 篇文章 0 订阅
1 篇文章 0 订阅

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(m1,i)0imaxIndex+1(n)}
解释:f(m)代表m个人,每人最多分到的水果数。将此时的分水果方法记到数组count中,count[i]代表第i种水果将分给平均分给count[i]人。
第m人考虑选择那种水果的时候,前m-1个人选择水果的策略不会发生改变,可以用反证法证明。
g ( m − 1 , i ) g(m-1,i) g(m1,i)代表在 m − 1 m-1 m1个人的选择方案下,第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(g1,ssuger(g))
第g组不吃糖果的情况下,有多少牛牛吃上糖果: f ( g − 1 , s ) f(g-1, s) f(g1,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(g1,ssuger(g)),f(g1,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]);
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值