[NOIP 2000 普及组] 税收与补贴问题

GitHub跳转原题关系图返回列表

把每个竞争售价转成关于税收或补贴 k 的一次严格不等式,和目标价对应约束求交集后,直接取绝对值最小的整数解。

OJ: luogu

题目 ID: P1023

难度:普及+/提高

标签:模拟枚举分段函数思维

日期: 2026-06-20 09:42

题意

给出一个商品的成本价、若干个“售价-销量”已知点,以及超过最后一个已知点后销量的线性下降速度。

要求找到一个整数税收或补贴金额,使得商家把售价定在政府预期售价时,获得的总利润严格大于其他所有可选售价,并且这个税收/补贴金额的绝对值最小。

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

using i64 = long long;

const int MAXM = 100005;

struct Fraction {
    i64 num; // 分子
    i64 den; // 分母,恒为正数
};

int target_price;
int point_cnt;
int tail_drop;
int raw_price[MAXM], raw_sale[MAXM];

// brute.cpp:教学用暴力。
// 直接枚举整数 k,并按题意计算每个价格下的销量和利润。

int cmp_fraction(Fraction a, Fraction b) {
    __int128 left = (__int128) a.num * b.den;
    __int128 right = (__int128) b.num * a.den;
    if (left < right) {
        return -1;
    }
    if (left > right) {
        return 1;
    }
    return 0;
}

Fraction get_sale_fraction(int x) {
    for (int i = 1; i < point_cnt; i++) {
        int x1 = raw_price[i];
        int y1 = raw_sale[i];
        int x2 = raw_price[i + 1];
        int y2 = raw_sale[i + 1];
        if (x1 <= x && x <= x2) {
            i64 num = 1LL * y1 * (x2 - x1) + 1LL * (y2 - y1) * (x - x1);
            i64 den = x2 - x1;
            return {num, den};
        }
    }

    int last_price = raw_price[point_cnt];
    int last_sale = raw_sale[point_cnt];
    return {1LL * last_sale - 1LL * tail_drop * (x - last_price), 1};
}

int get_upper_price() {
    int last_price = raw_price[point_cnt];
    int last_sale = raw_sale[point_cnt];
    if (tail_drop == 0) {
        return last_price;
    }
    return last_price + (last_sale - 1) / tail_drop;
}

// 返回售价 x 在税收/补贴 k 下的总利润。
// 这里利润也保存成分数,避免插值时的除法误差。
Fraction get_profit(int x, int k, int cost_price) {
    Fraction sale = get_sale_fraction(x);
    return {sale.num * (x - cost_price + k), sale.den};
}

bool check(int k, int cost_price, int upper_price) {
    if (target_price < cost_price || target_price > upper_price) {
        return false;
    }

    Fraction target_sale = get_sale_fraction(target_price);
    if (target_sale.num <= 0) {
        return false;
    }

    Fraction target_profit = get_profit(target_price, k, cost_price);

    for (int x = cost_price; x <= upper_price; x++) {
        if (x == target_price) {
            continue;
        }
        Fraction sale = get_sale_fraction(x);
        if (sale.num <= 0) {
            continue;
        }

        Fraction profit = get_profit(x, k, cost_price);
        if (cmp_fraction(profit, target_profit) >= 0) {
            return false;
        }
    }
    return true;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> target_price;
    while (true) {
        int x, y;
        cin >> x >> y;
        if (x == -1 && y == -1) {
            break;
        }
        raw_price[++point_cnt] = x;
        raw_sale[point_cnt] = y;
    }
    cin >> tail_drop;

    int cost_price = raw_price[1];
    int upper_price = get_upper_price();

    // 暴力范围开大一些,供随机对拍使用。
    for (int d = 0; d <= 2000; d++) {
        if (check(d, cost_price, upper_price)) {
            cout << d << '\n';
            return 0;
        }
        if (d != 0 && check(-d, cost_price, upper_price)) {
            cout << -d << '\n';
            return 0;
        }
    }

    cout << "NO SOLUTION\n";
    return 0;
}

这题本质上没有复杂数据结构,关键是把题意翻译成正确的不等式。

先约定:

  • k > 0 表示补贴
  • k < 0 表示税收
  • 利润函数是:

profit(price) = (price - cost + k) * sale(price)

其中 sale(price) 是一个离散的分段线性函数:

  • 在相邻给定点之间做整数线性插值
  • 超过最后一个点后按固定斜率递减

设目标售价是 tp,某个竞争售价是 x,它们对应销量分别是 stsx

若目标价要严格更优,就必须满足:

(tp - cost + k) * st > (x - cost + k) * sx

移项后得到一个关于 k 的一次严格不等式:

k * (st - sx) > (x - cost) * sx - (tp - cost) * st

也就是说:

  • 每个竞争价格 x 都会对 k 给出一个限制
  • 所有限制同时成立时,目标价才会成为唯一最优点

于是做法变成:

  1. 写一个 get_sale(price),按题意求出某个售价的销量
  2. 枚举所有可能和目标价竞争的整数售价 x
  3. 把“目标价利润严格大于 x 的利润”改写成对 k 的上下界限制
  4. 把所有限制求交集
  5. 在可行整数区间里取绝对值最小的 k

一个小表格

含义
price 商家选择的售价
sale(price) 该售价下的销量
k 税收/补贴金额,补贴为正,税收为负
profit(price) 总利润

这里还有两个细节:

  • 题目要求的是严格最大,所以遇到“相等利润”也算不合法
  • 如果目标价本身已经没有正销量,或者所有限制求交后区间为空,就输出 NO SOLUTION

代码

cpp
#include <bits/stdc++.h>
using namespace std;

using i64 = long long;

const int MAXM = 100005;
const i64 INF64 = (1LL << 60);

struct Fraction {
    i64 num; // 分子
    i64 den; // 分母,恒为正数
};

int target_price;
int point_cnt;
int tail_drop;
int raw_price[MAXM], raw_sale[MAXM];

// 比较两个分数 a 和 b 的大小,返回:
// -1: a < b
//  0: a = b
//  1: a > b
int cmp_fraction(Fraction a, Fraction b) {
    __int128 left = (__int128) a.num * b.den;
    __int128 right = (__int128) b.num * a.den;
    if (left < right) {
        return -1;
    }
    if (left > right) {
        return 1;
    }
    return 0;
}

// 计算 floor(a / b),其中 b > 0,支持 a 为负数。
i64 floor_div(i64 a, i64 b) {
    if (a >= 0) {
        return a / b;
    }
    return -((-a + b - 1) / b);
}

// 计算 ceil(a / b),其中 b > 0,支持 a 为负数。
i64 ceil_div(i64 a, i64 b) {
    if (a >= 0) {
        return (a + b - 1) / b;
    }
    return -((-a) / b);
}

// 返回售价 x 对应的销量。
// 为了避免中间插值产生误差,这里把销量写成分数。
Fraction get_sale_fraction(int x) {
    for (int i = 1; i < point_cnt; i++) {
        int x1 = raw_price[i];
        int y1 = raw_sale[i];
        int x2 = raw_price[i + 1];
        int y2 = raw_sale[i + 1];
        if (x1 <= x && x <= x2) {
            i64 num = 1LL * y1 * (x2 - x1) + 1LL * (y2 - y1) * (x - x1);
            i64 den = x2 - x1;
            return {num, den};
        }
    }

    int last_price = raw_price[point_cnt];
    int last_sale = raw_sale[point_cnt];
    return {1LL * last_sale - 1LL * tail_drop * (x - last_price), 1};
}

// 计算某个给定售价下,销量变成非正数前最后还需要检查到哪个价格。
int get_upper_price() {
    int last_price = raw_price[point_cnt];
    int last_sale = raw_sale[point_cnt];

    if (tail_drop == 0) {
        // 题面通常不会出现这种情况;若真的出现,销量不会继续下降,
        // 说明高价可以无限延伸,这时利润不会有唯一最大值。
        return last_price;
    }

    // 当销量 > 0 时才有意义,所以只需检查到最后一个正销量价格。
    return last_price + (last_sale - 1) / tail_drop;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> target_price;
    while (true) {
        int x, y;
        cin >> x >> y;
        if (x == -1 && y == -1) {
            break;
        }
        raw_price[++point_cnt] = x;
        raw_sale[point_cnt] = y;
    }
    cin >> tail_drop;

    int cost_price = raw_price[1];
    int upper_price = get_upper_price();

    // 目标价本身如果已经没有正销量,那么一定无解。
    Fraction target_sale = get_sale_fraction(target_price);
    if (target_price < cost_price || target_price > upper_price || target_sale.num <= 0) {
        cout << "NO SOLUTION\n";
        return 0;
    }

    // 对每个竞争价格 x,都有:
    // (tp - cost + k) * st > (x - cost + k) * sx
    // 化简成:
    // k * (st - sx) > (x - cost) * sx - (tp - cost) * st
    // 这是关于 k 的一次严格不等式。
    i64 low = -INF64; // k 的下界,满足 k >= low
    i64 high = INF64; // k 的上界,满足 k <= high

    for (int x = cost_price; x <= upper_price; x++) {
        if (x == target_price) {
            continue;
        }

        Fraction sale_x = get_sale_fraction(x);
        if (sale_x.num <= 0) {
            continue;
        }

        __int128 a128 = (__int128) target_sale.num * sale_x.den - (__int128) sale_x.num * target_sale.den;
        __int128 b128 = (__int128) (x - cost_price) * sale_x.num * target_sale.den
            - (__int128) (target_price - cost_price) * target_sale.num * sale_x.den;

        i64 a = (i64) a128;
        i64 b = (i64) b128;

        if (a == 0) {
            // 这时不等式退化成 0 > b。
            // 若不成立,说明无论怎么调税收/补贴,目标价都无法严格更优。
            if (!(0 > b)) {
                cout << "NO SOLUTION\n";
                return 0;
            }
            continue;
        }

        if (a > 0) {
            // k > b / a,所以 k >= floor(b / a) + 1
            low = max(low, floor_div(b, a) + 1);
        }
        else {
            // k < b / a。
            // 由于 a < 0,改写成 k * (-a) < (-b),其中 (-a) > 0。
            // 对整数来说,等价于:
            // k <= ceil((-b) / (-a)) - 1
            high = min(high, ceil_div(-b, -a) - 1);
        }
    }

    if (low > high) {
        cout << "NO SOLUTION\n";
        return 0;
    }

    // 在整数区间 [low, high] 中找绝对值最小的 k。
    i64 answer;
    if (low <= 0 && 0 <= high) {
        answer = 0;
    }
    else if (high < 0) {
        answer = high;
    }
    else {
        answer = low;
    }

    cout << answer << '\n';

    return 0;
}

复杂度

  • 设给出的已知点数量为 m,可行整数售价数量为 P
  • 求每个售价的限制时,需要沿分段函数找到它落在哪一段
  • 总复杂度约为 O(Pm)O(P * m)
  • 空间复杂度 O(m)O(m)

总结

这题最容易出错的不是复杂度,而是下面几个细节:

  • 补贴要加到单位利润里
  • 税收要从单位利润里减掉
  • 目标价必须严格优于其他所有售价,不能并列
  • 相邻已知点之间的销量是线性插值
  • 最后一个点之后如果无论怎么调都无法让目标价最优,要输出 NO SOLUTION