P1070 · OFFICIAL SOLUTION

P1070 晨汐散余香Ⅱ 官方题解

Gioush OJ · P1070 晨汐散余香Ⅱ

【Recall 晨汐散余香】

  • 原题中每份香料只有一个单位,具有收益 wiw_i 与失香时间 tit_i,每天至多交易一份。询问只限制最多选择 pp 个交易日。
  • 将香料按收益从大到小扫描,把当前香料放在不晚于 tit_i 的最晚空闲日。若不存在这样的日期,就舍弃当前香料。
  • 依次成功放入的香料构成序列 b1,b2,b_1,b_2,\ldots。前 rr 份就是所有可行的 rr 份香料中收益最大的方案,因此记录收益前缀即可回答全部询问。

【Recall 正确性】

  • 把当前香料安排到最晚空位不会占用更早的日期,所以不会使之后失香时间更早的香料更难加入。
  • 固定一个收益下界,只看收益不低于它的香料。上述过程会选出其中数量最多的可行集合,否则可以把遗漏的香料加入并反复交换到更晚的位置,与当前香料无法放入矛盾。
  • 因此前 rr 个成功单位的第 rr 大收益不会小于任意可行 rr 元方案的第 rr 大收益。对每个位置分别比较,便得到答案前缀的最优性。

【部分分:测试点 1∼41\sim 41∼4】

  • 总库存不超过 2020。可以枚举每个单位香料是否售出,再枚举或搜索这些香料被安排到哪些交易位置。
  • 若某个单位被安排在失香时间之后,或者某一天使用了超过 mm 个交易位置,这个方案不合法。同一种香料首次售出时再加入一次初售赏金。
  • 时间复杂度为指数级。这一档已经完整保留了截止时间、每日容量和只出现一次的额外收益。

【部分分:测试点 5∼85\sim 85∼8】

  • 先推广每天的交易次数。把第 dd 天拆成编号为 m(d1)+1mdm(d-1)+1\sim mdmm 个位置,那么在第 dd 天失香的单位,其截止位置就是 mdmd

  • 再将每种香料展开成 cic_i 个单位。若 xi>0x_i>0,第 rr 个单位的失香时间为 r/xi\lceil r/x_i\rceil;若 xi=0x_i=0,它在当前询问内始终有效。

  • 展开以后,每个单位仍然只有“收益”和“截止位置”两个属性,这已经与 Recall 中的问题完全相同。

  • 还需要处理初售赏金。对第 ii 种香料,取失香时间最晚的一个单位,把它的收益设为 ai+sia_i+s_i,其余单位的收益均设为 aia_i

  • 若某个方案售出了这种香料,却没有售出这个单位,就用它替换任意一个已经售出的同类单位。收益不变,截止时间只会变晚,所以方案仍然合法。

  • 于是初售赏金也被转化成了一个普通单位的收益。对每次询问重新运行 Recall 中的贪心,设总库存为 ss,时间复杂度为 O(qslogs)\mathcal O(qs\log s)

【部分分:测试点 9∼129\sim 129∼12】

  • P=maxpjP=\max p_j,只对前 PP 天建立 mPmP 个位置,并运行一次推广后的贪心。每成功放入一个单位,就记录当前收益前缀。
  • 对询问 pp,只需要前 mpmp 个成功单位。它们原本已经能够合法安排,把所占位置依次向左压缩后,一定可以全部放入前 mpmp 个位置。
  • 因此询问仍然只是读取一个答案前缀。时间复杂度为 O(slogs+q)\mathcal O(s\log s+q),与原题相比仅仅把可用位置数从 pp 改成了 mpmp

【部分分:测试点 13∼1513\sim 1513∼15】

  • 这一档中 mP3×103mP\leq3\times10^3。无论库存有多大,答案中真正能够出现的单位都不会超过 mPmP 个,所以没有必要展开其余单位。
  • 同一种香料的普通单位收益均为 aia_i,并且可以按照失香时间分成若干批。我们只记录当前失香时间最晚的一批,整批处理完成后再生成前一批。
  • 用优先队列维护各香料当前能够生成的最高收益,用并查集寻找最晚空位。这样恰好是在不改变 Recall 贪心顺序的前提下,延迟生成它真正访问到的单位。

【部分分:测试点 16∼1916\sim 1916∼19】

  • 特殊性质保证所有 xi=0x_i=0,所以全部单位都可以在任意交易日售出,不再需要处理截止位置。
  • 每种香料提供一个收益为 ai+sia_i+s_i 的单位,以及至多 ci1c_i-1 个收益为 aia_i 的单位。把这些带数量的收益段从大到小排序并求前缀即可。
  • 时间复杂度为 O(nlogn+qlogn)\mathcal O(n\log n+q\log n)。这一档说明普通单位可以整批保存,完整数据只需要为每一批重新带上截止位置。

【正解】

  • P=maxpjP=\max p_j。一天拆成 mm 个位置后,截止第 dd 天对应的位置上界为 mdmd,我们只需要维护前 mPmP 个位置。
  • xi>0x_i>0,第 ii 种香料在询问范围内的最晚截止日为
Di=min(P,cixi).D_i=\min\left(P,\left\lceil\frac{c_i}{x_i}\right\rceil\right).

xi=0x_i=0 时,令 Di=PD_i=P

  • 先生成一个截止日为 DiD_i、收益为 ai+sia_i+s_i 的单位。自然截止日超过 PP 的普通单位合并到第 PP 天,其余普通单位按照相同截止日组成批次。

  • 优先队列中只保存每种香料当前应该处理的单位或批次,并按照收益从大到小取出。初售单位的收益不小于同类普通单位,所以尚未生成的单位不可能越过它提前出现。

  • 对截止日为 dd 的单位,用并查集查询不超过 mdmd 的最晚空位。占用位置 rr 后,将它指向 r1r-1 左侧最近的空位,这与 Recall 中的实现完全相同。

  • 当前批次全部放入后,才生成这种香料截止时间更早的下一批。这样优先队列始终给出了逐单位展开后,下一个应该被贪心扫描的收益。

  • 若一个批次只能放入一部分,最终一定是因为不超过其截止位置的空位已经全部占满。后续同类批次的截止时间只会更早,所以它们也不可能再被放入。

  • 每成功放入一个单位,就记录一次收益前缀。设最终成功放入 CC 个单位,询问 pjp_j 的答案就是前 min(mpj,C)\min(mp_j,C) 项收益之和。

  • 到这里,算法仍然是 aroma 的“收益降序、占用最晚空位、记录答案前缀”,只是用批次与优先队列避免了完整展开库存。

  • 优先队列初始加入 nn 个初售单位,之后至多有 mPmP 个单位真正占用交易位置,因此总处理规模为 O(n+mP)\mathcal O(n+mP)

  • P=maxpjP=\max p_j。总时间复杂度为 O((n+mP)logn+q)\mathcal O((n+mP)\log n+q),空间复杂度为 O(n+mP)\mathcal O(n+mP)

  • 收益前缀可能达到 101510^{15} 量级,需要使用 long long。多组数据之间还要清空优先队列,并重新初始化前 mPmP 个并查集位置。

【参考代码】

/*Author:EhundateghDate:2026/8/19Name:scent.cppYou steal,I kill.*/#include <queue>#include <vector>#include <cstdio>#include <cstring>#include <algorithm>#define MAXN 100010#define MAXS 1000010using namespace std; int c,T,n,m,q,Fa[MAXS],Ask[MAXN];long long Pref[MAXS]; int Find(int x) {    int y=x;    while (Fa[y]!=y) y=Fa[y];    while (Fa[x]!=x) {        int Temp=Fa[x];        Fa[x]=y;        x=Temp;    }    return y;} struct type{    long long a,s,c,x,Count,Remain;    int Day,MaxDay;    bool Bonus;    void Init(int p) {        MaxDay=p;        Bonus=true;        Day=x==0?p:(int)min<long long>(p,(c+x-1)/x);        Count=1;        Remain=c-1;    }    bool Next() {        if (Bonus) {            Bonus=false;            if (!Remain||!MaxDay) return false;            if (!x) {                Day=MaxDay;                Count=Remain;                return true;            }            long long Last=(c+x-1)/x;            if (Last>=MaxDay) {                Day=MaxDay;                Count=c-x*(MaxDay-1)-1;            }            else {                Day=(int)Last;                Count=c-x*(Last-1)-1;            }            if (Count>0) return true;        }        else {            Remain-=Count;            if (!Remain||!x) return false;            Day--;            Count=min(x,Remain);            return Day>0;        }        while (Remain>0&&Day>1) {            Day--;            Count=min(x,Remain);            if (Count>0) return true;        }        return false;    }    long long Get(){return a+(Bonus?s:0);}}A[MAXN]; struct node{    long long Value;    int Id;    bool operator <(const node &Other)const{        if (Value!=Other.Value) return Value<Other.Value;        return Id>Other.Id;    }}; priority_queue <node> Q; void Solve() {    scanf("%d%d%d",&n,&m,&q);    for (int i=1;i<=n;i++) scanf("%lld%lld%lld%lld",&A[i].a,&A[i].s,&A[i].c,&A[i].x);    int MaxP=0;    for (int i=1;i<=q;i++) {        scanf("%d",&Ask[i]);        MaxP=max(MaxP,Ask[i]);    }    int Capacity=m*MaxP,Count=0;    for (int i=0;i<=Capacity;i++) Fa[i]=i;    while (!Q.empty()) Q.pop();    Pref[0]=0;    for (int i=1;i<=n;i++) {        A[i].Init(MaxP);        Q.push({A[i].Get(),i});    }    while (!Q.empty()&&Count<Capacity) {        node Temp=Q.top();Q.pop();        type &Now=A[Temp.Id];        int Limit=m*Now.Day;        long long Used=0;        while (Used<Now.Count) {            int Pos=Find(Limit);            if (!Pos) break;            Fa[Pos]=Find(Pos-1);            Count++;            Pref[Count]=Pref[Count-1]+Temp.Value;            Used++;            if (Count==Capacity) break;        }        if (Used==Now.Count&&Now.Next()) Q.push({Now.Get(),Temp.Id});    }    for (int i=1;i<=q;i++) {        int Take=min(m*Ask[i],Count);        printf("%lld\n",Pref[Take]);    }    return;} int main() {    scanf("%d%d",&c,&T);    while (T-->0) Solve();    return 0;}