[每日一题] 79. 合唱团(动态规划、网易17系列)

1. 题目来源

链接:合唱团
来源:牛客网

2. 题目说明

有 n 个学生站成一排,每个学生有一个能力值,牛牛想从这 n 个学生中按照顺序选取 k 名学生,要求相邻两个学生的位置编号的差不超过 d,使得这 k 个学生的能力值的乘积最大,你能返回最大的乘积吗?

输入描述:

每个输入包含 1 个测试用例。每个测试数据的第一行包含一个整数 n (1 <= n <= 50),表示学生的个数,接下来的一行,包含 n 个整数,按顺序表示每个学生的能力值 ai(-50 <= ai <= 50)。接下来的一行包含两个整数,k 和 d (1 <= k <= 10, 1 <= d <= 50)。

输出描述:

输出一行表示最大的乘积。

示例:

输入
3
7 4 7
2 50
输出
49

3. 题目解析

思路一

题目要求n各学生中选择k个,使这k个学生的能力值乘积最大。这是一个最优化的问题。另外,在优化过程中,提出了相邻两个学生的位置编号差不超过d的约束。
解决的方法是采用动态规划:1.求解的是最优化问题;2.可以分解为最优子结构

对该问题的分解是关键。 从n个学生中,选择k个,可以看成是:先从n个学生里选择最后1个,然后在剩下的里选择k-1个,并且让这1个和前k-1个满足约束条件 记第k个人的位置为one,则可以用 f[one][k] 表示从n个人中选择k个的方案。然后,它的子问题,需要从one前面的left个人里面,选择k-1个,这里left表示k-1个人中最后一个(即第k-1个)人的位置,因此,子问题可以表示成 f[left][k-1] 。

一般的动态规划题目,中间使用的表的最后一个元素, dp[N][K] 就是所求的结果。 但这个题目不能这样,因为如果那样建表,子问题就成了“在前n个学生中,取k个,使乘积最大” 然而,本题目有额外的限制条件“相邻两个学生的位置编号的差不超过d”就没有办法代入递推公式了, 因为子问题中本身并不包含位置信息。

其次,求最大乘积比求最大和的问题要复杂许多。求最大和的话,子问题中也只需要求最大和就行了。 但求最大乘积的时候,在子问题中,每一步需要求最大正积和最小负积。 因为如果某学生的能力值为负数,乘以前面求得的最小负积,结果才是最大乘积。

思路二:

状态的定义:f [ i ] [ j ] [ 最大 / 最小 ]
分别表示:以第i个人为最后一个(也是必选的)人,加上这个人,已经选了 j 个人,最大可能的乘积和最小可能的乘积。

为什么不是只记录最大的,还要记录最小的?
因为最小的,很可能是一个负数,有着极大的绝对值,再乘一个负数,就变成最大的正数,也就是最优解了。

然后考虑,这个状态由哪些状态转移过来?
j 人,明显是从j-1个人的状态,最后加1个人(当前考虑的 i )而来。

第 i 人,根据题目要求,编号差不能大于d。那我们就往前观察最多d个人,从i-d到i-1,选了j-1个人中,选择和自己相乘,最大/最小的。

注意考虑边界条件:只选了一个人,就是 i 自己。
最后,解很大,使用long long(C++)来保存中间计算结果。

4. 代码展示

思路一:

#include <iostream>
#include <string>
#include <vector>
#include <limits.h>         // 宏 LONG_MIN LONG_MAX
 
using namespace std;
 
// NK表存放的是,在前n个学生中,取k个(必取第n个),所等得到的最大正乘积和最小负乘积。
long long NK_zheng[51][11];
long long NK_fu[51][11];
 
int main() {
    int N;
    cin >> N;
    vector<int> V(N + 1);
    for (int i = 1; i <= N; i++) 
        cin >> V[i];
    int K, D;
    cin >> K >> D;
 
    // 对数组先初始化
    for (int n = 1; n <= 50; n++) {
        for (int k = 1; k <= 10; k++) {
            NK_zheng[n][k] = LLONG_MIN;     // 代表无效值
            NK_fu[n][k] = LLONG_MAX;
        }
    }
 
    for (int n = 1; n <= N; n++) {
        if (V[n] >= 0)
            NK_zheng[n][1] = V[n];
        else
            NK_fu[n][1] = V[n];
    }
 
    for (int n = 2; n <= N; n++) {
        for (int k = 2; k <= K && k <= n; k++) {
            // 找到NK[...][k-1]中最大的正数和最小的负数。
            long long max = LLONG_MIN, min = LLONG_MAX;
            for (int temp = (k - 1 > n - D) ? (k - 1) : (n - D); temp <= n - 1; temp++) {     
            	// 起始位置很重要
            	if (max < NK_zheng[temp][k - 1])
                	max = NK_zheng[temp][k - 1];
            	if (min > NK_fu[temp][k - 1])
                	min = NK_fu[temp][k - 1];
            }
            if (max != LLONG_MIN) {
                if (V[n] >= 0)
                    NK_zheng[n][k] = max * V[n];
                else
                    NK_fu[n][k] = max * V[n];               
            }
            if (min != LLONG_MAX) {
                if (V[n] < 0 && NK_zheng[n][k] < min * V[n])
                    NK_zheng[n][k] = min * V[n];
                else if (NK_fu[n][k] > min * V[n])
                    NK_fu[n][k] = min * V[n];
            }
        }
    }
    // 在两个表的最后一列中,找出最大的乘积。
    long long max1 = LLONG_MIN, max2 = LLONG_MIN;

    for (int n = K; n <= N; n++) {
        if (max1 < NK_zheng[n][K])
            max1 = NK_zheng[n][K];
        if (max2 < NK_fu[n][K] && NK_fu[n][K] != LLONG_MAX)
            max2 = NK_fu[n][K];
    }
 
    max1 = max1 > max2 ? max1 : max2;
    
    cout << max1 << endl;
    return 0;
    
//这一组自定义输入没过  这里输出-40,应该为-24,牛客没这组测试
//4
//-5 -4 -3 -2
//3 50
}

思路二:

// 网易2017校招内推笔试之编程题
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <math.h>
#include <algorithm>
#include <bits/stdc++.h>
using namespace std;
 
int a[55];
long long f[55][15][2];
 
int main() {
    int n, kk, d;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }
    cin >> kk >> d;
    long long ans = LLONG_MIN;
    for (int i = 1; i <= n; ++i) {
        f[i][1][0] = f[i][1][1] = a[i];
        for (int j = 2; j <= kk; ++j){
            for (int k = i - 1; k >= max(i - d, 1); --k) {
                f[i][j][0] = max(f[i][j][0], max(f[k][j - 1][0] * a[i], f[k][j - 1][1] * a[i]));
                f[i][j][1] = min(f[i][j][1], min(f[k][j - 1][0] * a[i], f[k][j - 1][1] * a[i]));
            }
        }
        ans = max(ans, max(f[i][kk][0],f[i][kk][1]));
    }
    cout << ans << endl;
    return 0;
}

//这一组自定义输入没过  这里输出0,应该为-24,牛客没这组测试
//4
//-5 -4 -3 -2
//3 50
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值