♐ CSP_J 组真题
2015年
4. 推销员

线上OJ:

一本通:http://ybt.ssoier.cn:8088/problem_show.php?pid=1972 (opens in a new tab)
洛谷:https://www.luogu.com.cn/problem/P2672 (opens in a new tab)
AcWing:https://www.acwing.com/problem/content/466/ (opens in a new tab)

核心思想:

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

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号,第二个该选谁?
假设候选人在5号的左侧,由 “性质1” 可知,左侧的住户由于 node[i].s < node[5].s ,故 s 都不参与最终疲劳值的计算,只有 node[i].a 参与计算。所以,如果在 左侧选,就选 node[i].a 最大的 1 号。因为 node[1].a = 10,是左侧剩余4个中最大的。
假设候选人在5号的右侧,则 node[i].s > node[5].s。所以右侧住户的 a 和 s 都要参与考虑。我们记右侧住户带来的 疲劳值增量node[i].a+Δsnode[i].a + Δs, 其中 Δs=2(node[i].snode[i].5)Δs = 2 * ( node[i].s - node[i].5 )。由于若选择了更远的s(比如),则更远的 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 更大,则输出 ans+maxLans+maxL,此时 now 不变。
b. 如果 maxR 更大,则输出 ans+node[i].a+Δsans+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),其实是有风险的。好在数据都过了,说明测试数据中并没有极端数据存在。