【算法】小团的车辆调度(美团2021校招题)

1. 概述

牛客OJ链接

小团是美团汽车租赁公司的调度师,某个时刻A和B两地都向该公司提交了租车的订单,分别需要a和b辆汽车。此时,公司的所有车辆都在外运营;通过北斗定位,可以得到所有车辆的位置,小团分别计算了每辆车前往A地和B地完成订单的利润。作为一名精明的调度师,当然是想让公司的利润最大化了。请你帮他分别选择a辆车完成A地的任务,选择b辆车完成B地的任务,使得公司获利最大,每辆车最多只能完成一地的任务。

输入描述

输入第一行包含三个整数n, a, b, 分别表示公司的车辆数量和A, B两地订单所需数量,保证a + b <= n(1 <= n <= 2000),接下来有n行,每行两个正整数x, y, 分别表示该车完成A地任务的利润和完成B地任务的利润。

输出描述

输出仅包含一个正整数,表示公司最大获得的利润和。

样例输入
5 2 2
4 2
3 3
5 4
5 3
1 5
样例输出
18

 

2. 解题思路

DFS可以做,但肯定是超时的
这里要用到三维的动态规划,还需要优化三维dp数组到二维,并且减除调不必要的循环,才能满足要求。

动态规划
dp[i][j][k]表示前 i 辆车中派出 j 辆到A地,派出 k 辆到B地可以获得的最大利润

状态转移方程

在这里插入图片描述

 

3. 代码及优化

Java

import java.util.Scanner;

public class Main{
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int a = input.nextInt();
        int b = input.nextInt();
        int[][][] dp = new int[n + 1][a + 1][b + 1];
        int[][] profits = new int[n + 1][2];
        for (int i = 1; i <= n; i++) {
            profits[i][0] = input.nextInt();
            profits[i][1] = input.nextInt();
        }
        for (int i = 1; i <= n; i++) { //派出i辆车
            for (int j = 0; j <= a; j++) { //向A地派出j辆
                for (int k = 0; k <= b; k++) { //向B地派出k辆
                    if (j == 0 && k == 0) continue;
                    if (k == 0) {
                        //将第i辆车不派出或者派到A地
                        dp[i][j][k] = Math.max(dp[i - 1][j][k], dp[i - 1][j - 1][k] + profits[i][0]);
                    }
                    else if (j == 0) {
                        //将第i辆车不派出或者派到B地
                        dp[i][j][k] = Math.max(dp[i - 1][j][k], dp[i - 1][j][k - 1] + profits[i][1]);
                    }
                    else {
                        //将第i辆车不派出或者派到A地或者派到B地
                        dp[i][j][k] = Math.max(dp[i - 1][j][k],
                                Math.max(dp[i - 1][j - 1][k] + profits[i][0], dp[i - 1][j][k - 1] + profits[i][1]));
                    }
                }
            }
        }
        int res = dp[n][a][b];
        System.out.println(res);
    }
}

根据动态规划转移方程可看出,状态 i 只取决于状态 i - 1,因此可以像优化 01背包问题 那样将dp数组降维,降低空间复杂度。

因为状态 i 需要用到状态 i - 1 时的值,如果从小到大计算,则需要的 i - 1时的状态已经被覆盖,因此要从右至左地计算。

代码

import java.util.Scanner;

public class Main{
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int a = input.nextInt();
        int b = input.nextInt();
        int[][] dp = new int[a + 1][b + 1];
        int[][] profits = new int[n + 1][2];
        for (int i = 1; i <= n; i++) {
            profits[i][0] = input.nextInt();
            profits[i][1] = input.nextInt();
        }
        for (int i = 1; i <= n; i++) { //派出i辆车
            for (int j = a; j >= 0; j--) { //向A地派出j辆
                for (int k = b; k >= 0; k--) { //向B地派出k辆
                    if (j == 0 && k == 0) continue;
                    if (k == 0) {
                        //将第i辆车不派出或者派到A地
                        dp[j][k] = Math.max(dp[j][k], dp[j - 1][k] + profits[i][0]);
                    }
                    else if (j == 0) {
                        //将第i辆车不派出或者派到B地
                        dp[j][k] = Math.max(dp[j][k], dp[j][k - 1] + profits[i][1]);
                    }
                    else {
                        //将第i辆车不派出或者派到A地或者派到B地
                        dp[j][k] = Math.max(dp[j][k],
                                Math.max(dp[j - 1][k] + profits[i][0], dp[j][k - 1] + profits[i][1]));
                    }
                }
            }
        }
        int res = dp[a][b];
        System.out.println(res);
    }
}

还可以看到,for循环里面是可以优化的,有些计算是无用的。例如,派出了 i 辆车,如果 j > i 或者 k > i 是没必要计算的,一共 i 辆,不可能派出比 i 辆车更多的车到A地或者B地,因此,在从右至左计算时,从 Math.min(a, i) 和 Math.min(b, i) 开始。

代码

import java.util.Scanner;

public class Main{
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int a = input.nextInt();
        int b = input.nextInt();
        int[][] dp = new int[a + 1][b + 1];
        int[][] profits = new int[n + 1][2];
        for (int i = 1; i <= n; i++) {
            profits[i][0] = input.nextInt();
            profits[i][1] = input.nextInt();
        }
        for (int i = 1; i <= n; i++) { //派出i辆车
            for (int j = Math.min(a, i); j >= 0; j--) { //向A地派出j辆
                for (int k = Math.min(b, i); k >= 0; k--) { //向B地派出k辆
                    if (j == 0 && k == 0) continue;
                    if (k == 0) {
                        //将第i辆车不派出或者派到A地
                        dp[j][k] = Math.max(dp[j][k], dp[j - 1][k] + profits[i][0]);
                    }
                    else if (j == 0) {
                        //将第i辆车不派出或者派到B地
                        dp[j][k] = Math.max(dp[j][k], dp[j][k - 1] + profits[i][1]);
                    }
                    else {
                        //将第i辆车不派出或者派到A地或者派到B地
                        dp[j][k] = Math.max(dp[j][k],
                                Math.max(dp[j - 1][k] + profits[i][0], dp[j][k - 1] + profits[i][1]));
                    }
                }
            }
        }
        int res = dp[a][b];
        System.out.println(res);
    }
}

还可以继续优化,在每次 i 循环时,其实没必要将其派到A地或者B地的利润保存下来,因为只在本轮循环有用,直接输入使用就行了,可以节省大量空间。

另外,为了降低算法的时间复杂度,可以想尽办法避免一些不必要的计算。
根据上面的代码,考虑前 i 辆车时:

派到A地最多Math.min(a, i)辆车,即 j <= Math.min(a, i),
A地一共需要a辆车,已经派去A地 j 辆车,还需要 a - j 辆车;公司一共有n辆车,派出去了 i 辆,还剩下 n - i 辆,要满足所有的要求,则剩下的车还要满足A地的需求,必有: a - j <= n - i,即剩下的车要比A地还需要的车多,否则没有计算的必要了,由此得到 j >= a - n + i,且j >= 0,所以 j >= Math.max(0, a - n + i).

派到B地最多Math.min(b, i - j)辆车(i 辆车中已经派出 j 辆到A地了),还需要 b - k 辆车;公司一共n辆车,派出去了 i 辆,还剩下 n - i 辆,要满足所有的要求,则剩下的车还要同时满足A地和B地的需求,必有:(a - j) + (b - k) <= n - i,即剩下的车要比A地和B地还需要的车多,否则没有计算的必要了,由此可得到 k >= a + b - n + i - j,且 k >= 0,所以 k >= Math.max(0, a + b - n + i - j).

代码

import java.util.Scanner;

public class Main{
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int a = input.nextInt();
        int b = input.nextInt();
        int[][] dp = new int[a + 1][b + 1];
        int profitA, profitB;
        for (int i = 1; i <= n; i++) { //派出i辆车
            profitA = input.nextInt();
            profitB = input.nextInt();
            for (int j = Math.min(a, i); j >= Math.max(0, a - n + i); j--) { //向A地派出j辆
                for (int k = Math.min(b, i - j); k >= Math.max(0, a + b - n + i - j); k--) { //向B地派出k辆
                    if (j == 0 && k == 0) continue;
                    if (j + k > i) break;
                    if (k == 0) {
                        //将第i辆车不派出或者派到A地
                        dp[j][k] = Math.max(dp[j][k], dp[j - 1][k] + profitA);
                    }
                    else if (j == 0) {
                        //将第i辆车不派出或者派到B地
                        dp[j][k] = Math.max(dp[j][k], dp[j][k - 1] + profitB);
                    }
                    else {
                        //将第i辆车不派出或者派到A地或者派到B地
                        dp[j][k] = Math.max(dp[j][k],
                                Math.max(dp[j - 1][k] + profitA, dp[j][k - 1] + profitB));
                    }
                }
            }
        }
        int res = dp[a][b];
        System.out.println(res);
    }
}

再进一步简化,再进入下一层for循环前就计算出for循环的取值范围,可以避免在for循环中每次都进行冗余的计算,从而降低耗时。

import java.util.Scanner;

public class Main{
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int a = input.nextInt();
        int b = input.nextInt();
        int[][] dp = new int[a + 1][b + 1];
        int profitA, profitB;
        for (int i = 1; i <= n; i++) { //派出i辆车
            profitA = input.nextInt();
            profitB = input.nextInt();
            // 在进入下层for循环前,先计算出for循环的取值范围,避免在for循环中每次都进行冗余的计算,从而降低耗时
            int minj = Math.max(0, a - n + i);
            for (int j = Math.min(a, i); j >= minj; j--) { //向A地派出j辆
            	// 在进入下层for循环前,先计算出for循环的取值范围,避免在for循环中每次都进行冗余的计算,从而降低耗时
                int mink = Math.max(0, a + b - n + i - j);
                for (int k = Math.min(b, i - j); k >= mink; k--) { //向B地派出k辆
                    if (j == 0 && k == 0) continue;
                    if (j + k > i) break;
                    if (k == 0) {
                        //将第i辆车不派出或者派到A地
                        dp[j][k] = Math.max(dp[j][k], dp[j - 1][k] + profitA);
                    }
                    else if (j == 0) {
                        //将第i辆车不派出或者派到B地
                        dp[j][k] = Math.max(dp[j][k], dp[j][k - 1] + profitB);
                    }
                    else {
                        //将第i辆车不派出或者派到A地或者派到B地
                        dp[j][k] = Math.max(dp[j][k],
                                Math.max(dp[j - 1][k] + profitA, dp[j][k - 1] + profitB));
                    }
                }
            }
        }
        int res = dp[a][b];
        System.out.println(res);
    }
}

与上面的对比,运行耗时确实有了一些优化,正是减少了for循环中的冗余计算。

 
C++

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    int n, a, b;
    cin >> n >> a >> b;
    vector<vector<int>> dp(a + 1, vector<int>(b + 1, 0));
    int profitA, profitB;
    for (int i = 1; i <= n; i++) {
        cin >> profitA >> profitB;
        // 进入下一层for循环前先计算出取值范围,否则下层for循环中每次都会进行计算,徒增耗时,从而导致超时
        int minj = max(0, a - n + i);
        for (int j = min(a, i); j >= minj; j--) {
            // 进入下一层for循环前先计算出取值范围,否则下层for循环中每次都会进行计算,徒增耗时,从而导致超时
            int mink = max(0, a + b - n + i - j);
            for (int k = min(b, i - j); k >= mink; k--) {
                if (j == 0 && k == 0) continue;
                if (j + k > i) break;
                if (k == 0) {
                    //将第i辆车不派出或者派到A地
                    dp[j][k] = max(dp[j][k], dp[j - 1][k] + profitA);
                }
                else if (j == 0) {
                    //将第i辆车不派出或者派到B地
                    dp[j][k] = max(dp[j][k], dp[j][k - 1] + profitB);
                }
                else {
                    //将第i辆车不派出或者派到A地或者派到B地
                    dp[j][k] = max(dp[j][k],max(dp[j - 1][k] + profitA, dp[j][k - 1] + profitB));
                }
            }
        }
    }
    cout << dp[a][b] << endl;
    return 0;
}
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值