2015NOIP普及组真题 4. 推销员

线上OJ:

一本通:http://ybt.ssoier.cn:8088/problem_show.php?pid=1972

核心思想:

1、推销给每一个住户的疲劳值由两部分组成:
① 推销给住户的疲劳值 Ai ( < 1 0 3 10^3 103 )
② 从入口走到该住户再走回入口的疲劳值 2*Si(< 1 0 8 10^8 108

2、Ai 是每个住户都有的基本属性,都会参与计算。2*Si 只会在最远的 Si 处进行计算,其他 < Si 的可以在路过的时候推销,故 不是所有的 Si 都需要计算。(本条记为性质1

先分析这道题,由于给的测试数据只有一个,而且非常水,所以我们自己随机构建一组测试数据用于分析。

假设把每个住户看成一个结构体(则可如下所示)。
node[i].pl 为只推销这一个住户所产生的疲劳值,则node[i].pl 的最大值即为当 X=1(仅推销一个住户时)的解。

struct Node{
    int a, s; // a为Ai,s为Si
    int pl; // 如果直接到该住户的疲劳值
    int idx;// 记录该住户的编号
};
Node node[MAXN];

我们构造一组测试数据,如下图,可以发现:

如果只推销给一个人(X=1),那么应该选 idx 为 5 的。因为输出的是5号的疲劳值 node[5].pl = 20+2*7 = 34。很明显,34 是所有的里面最大的。

在这里插入图片描述

如果要推销给两个人(X=2),那么除了5号,第二个该选谁?
1、假设候选人在5号的左侧,由 “性质1” 可知,左侧的住户由于 node[i].s < node[5].s ,故 s 都不参与最终疲劳值的计算,只有 node[i].a 参与计算。所以,如果在 左侧选,就选 node[i].a 最大的 1 号。因为 node[1].a = 10,是左侧剩余4个中最大的。
2、假设候选人在5号的右侧,则 node[i].s > node[5].s。所以右侧住户的 a 和 s 都要参与考虑。我们记右侧住户带来的 疲劳值增量 n o d e [ i ] . a + Δ s node[i].a + Δs node[i].a+Δs, 其中 Δ s = 2 ∗ ( n o d e [ i ] . s − n o d e [ i ] . 5 ) Δs = 2 * ( node[i].s - node[i].5 ) Δs=2(node[i].snode[i].5)。由于若选择了更远的s(比如7号或者8号),则更远的 node[i].s 会包含原先的 node[i].5。所以,如果在 右侧选,就选 7 号。因为7号的增量为 9 + 2*(9-7) = 13,6号的增量为 3+2*(8-7)=5,8号的增量为 4 + 2*(11-7) = 12。7号的 增量最大

综上所述,在左侧和右侧的最大值中挑一个最大的,即可作为下一轮的候选。

解法一的核心思想:
1、先找到初始疲劳值最大的住户,作为 X=1 时的结果,直接输出。同时记录该住户的序号 为 now

2、在 now 的左侧寻找 node[i].a 的最大值,作为左侧最大值 maxL;在 now 的右侧寻找 node[i].a + Δs 最大值,作为右侧最大值 maxR。

3、在 maxL 和 maxR 中取大者作为下一轮的选择。
a. 如果 maxL 更大,则输出 a n s + m a x L ans+maxL ans+maxL,此时 now 不变。
b. 如果 maxR 更大,则输出 a n s + n o d e [ i ] . a + Δ s ans+node[i].a + Δs ans+node[i].a+Δs,此时 now 的位置要迁移到更大的 si 处。

注意1:由于每次都是在now的左侧和右侧寻找最大值,所以可以考虑用 两个优先队列 分别存储左侧和右侧。每次只需要从优先队列的 top 取出 合法 的数值即可。
注意2:如果右侧的增量更大,则记得更新 now 的位置至新的 si。同时由于now的位置发生了变化, now左右两侧的优先队列都需要更新

#include <bits/stdc++.h>
#define MAXN 100005
using namespace std;

struct Node{
    int a, s;
    int pl; // 如果直接到该住户的疲劳值
    int idx;// 记录该住户的编号
    bool operator <(const Node &a)const{
		return pl<a.pl;//以结构体中的ans(每一家推销的疲劳值)为比较对象
	}
};
Node node[MAXN];

priority_queue<Node> qR;
priority_queue<int> qL;

int n, now, maxL, maxR, ans;

// 模拟。每次比较左侧最大值和右侧最大值。如果是右侧最大值,则更新now的位置
int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)  scanf("%d", &node[i].s);
    for(int i = 1; i <= n; i++)  // 读入每个住户的a,s,疲劳值,以及索引号
    {
        scanf("%d", &node[i].a);
        node[i].pl = 2 * node[i].s + node[i].a;
        node[i].idx = i;
        qR.push(node[i]);  // 初始都加入右侧的qR优先队列
    }

    for(int i = 1; i <= n; i++)
    {
        maxL = maxR = 0;
        if(!qL.empty())
            maxL = qL.top();    // 如果now左侧不为空,则取出左侧最大的ai

        if(!qR.empty()) // 将now右边的最大pl值取出
            maxR = qR.top().pl;

        if(maxL < maxR - 2 * node[now].s) // 如果右边的最大值更大 (由于右边的距离更远,所以更远距离产生的疲劳值要减去now位置的距离疲劳值,才是右边最大值带来的疲劳值增幅)
        {
            ans += maxR - 2 * node[now].s;// 当前输出结果加上右侧最大值的增幅
            for(int k = now + 1; k < qR.top().idx; k++)
                qL.push(node[k].a);  // now和新位置所夹的住户疲劳值,都入左侧的优先队列

            now = qR.top().idx; // 更新now的坐标,并从qR弹出
            qR.pop();
            
            while(!qR.empty() && qR.top().idx <= now)
                qR.pop(); // 保证右侧优先队列的top是在新now的右边
        }
        else
        {
            ans += maxL; // 如果是左边的大,则输出结果直接加上maxL即可
            qL.pop();
        }

        printf("%d\n", ans);
    }
    return 0;
}

注:解法一的时间复杂度是 O(N2),其实是有风险的。好在数据都过了,说明测试数据中并没有极端数据存在。

思考:

一般这种线性的,for 循环后数据范围大的题目,很容易用到 dp 或者 前缀和 来优化。这道题也是一样,只是等量关系比较难找。具体可参照此处

考虑下图,依然是刚才的测试数据,但是我们按照 Ai 先进行降序排序。我们如果将 向 X 家住户推销产品的最大花费记为 ans[x],则 ans[x] 要么来自于 Ai 最大的 前 x 家;要么来自于 Ai 最大的 前 x−1 家,然后最后1家小的去换 x~n 中 pl 值最大的一家

在这里插入图片描述

如上图所示:
如果 X=1,则选择 5 号(因为Ai最大的一个是它,1 ~ n中 pl 值最大的也是它)
如果 X=2,则要么选择Ai最大的两个(5号和1号,总疲劳值为34+10=42);要么选择Ai最大的n-1=1个(5号),然后从2 ~ n中挑选 pl 值最大的(7号,总疲劳值为34+9+4=47)。由于47>42,所以选择7号。
如果 X=3,则要么选择Ai最大的三个(5号、1号和4号,总疲劳值为34+10+9=53),要么选择Ai最大的n-1=2个(5号和1号),然后从3 ~ n中挑选 pl 值最大的(7号,总疲劳值为34+10+9+4=57)。由于57>53,所以第三个选择7号。
如果 X=4,则要么选择Ai最大的四个(5号、1号、4号和7号,总疲劳值为20+10+9+9+2*9=66),要么选择Ai最大的n-1=3个(5号、1号和4号),然后从4 ~ n中挑选 pl 值最大的(正好也是7号)。

在这里还需要思考,为何只需要把最小的 Ai 替换成后续最大的 pl。能否将倒数第二小、倒数第三小的 Ai 也替换成第二大的 pl 等。
答案是不需要。因为

在这里插入图片描述

假设当X=3时,把最小的A3换成后续最大的A8,则会有如下两式:

A1 + A2 + A3 + 2 * max(S1, S2, S3) ①式
A1 + A2 + A8 + 2 * max(S1, S2, S8) ②式

由于A已经先按照降序排好序,所以 A3必 >= A8。如果此时 ①式 < ② 式,则 S8必 ≥ S3。
如果把倒数第二小 A2 也换掉,比如换成 Ai。此时我们知道以下确定条件
1、Ai < A3 < A2 (因为A已经排好序了)
2、Ai + 2 * Si < A8 + 2 * S8 (因为A8是后续最大值,Ai是次最大值)
此时的Si 存在两种可能性,要么 Si < S8 或者 Si > S8

如果 Si < S8,则 Si 的贡献被 S8 包含,次最小值实际最终的贡献值只有 Ai。但 Ai < A2, 所以替换只会越来越小,没有意义。
如果 Si > S8,由于 Ai + 2 * Si < A8 + 2 * S8,所以 Si - S8 的正向贡献 一定小于 Ai - A8 的负向贡献。也就是说,即使 Si 比 S8 大,带来了一些增量,但是 Ai 比 A8 减少的量更多。而 A8 < A2,所以 Ai 比 A2只会减少的量更多,所以替换次小值只会越来越小,没有意义。
所以,只需要将最小的 Ai 替换成后续最大的 pl 即可。不需要再考虑次最小值的替换。

题解代码:
#include <bits/stdc++.h>
#define MAXN 100005
using namespace std;

struct Node{
    int a, s;
    int pl; // 如果直接到该住户的疲劳值
};
Node node[MAXN];

bool cmp(Node x, Node y)
{
    return x.a > y.a;
}

int ans[100010];
int plmax[100010],smax[100010];
int n;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)  scanf("%d", &node[i].s);
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &node[i].a);
        node[i].pl = 2 * node[i].s + node[i].a;
    }

    sort(node + 1, node + 1 + n, cmp); // 按照推销疲劳值ai进行降序排序

    // 计算第i~n的节点中,疲劳值的最大值
    for(int i = n; i >= 1; i--)  plmax[i] = max(plmax[i+1], node[i].pl);

    // 计算前i个节点中,s的最大值
    for(int i = 1; i <= n; i++)  smax[i] = max(smax[i-1], node[i].s);

    // 计算前i个节点的推销疲劳值ai的和
    for(int i = 1; i <= n; i++)  ans[i] = ans[i-1] + node[i].a;

    // 每个x的输出为前 x 个最大的ai;或者前 x-1 个最大的ai,加一个i~n中最大的plmax[i]
    for(int i = 1; i <= n; i++)  printf("%d\n", max(ans[i-1] + plmax[i], ans[i] + 2 * smax[i]));

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值