P1070 晨汐散余香Ⅱ 官方题解
Gioush OJ · P1070 晨汐散余香Ⅱ
【Recall 晨汐散余香】
- 原题中每份香料只有一个单位,具有收益 与失香时间 ,每天至多交易一份。询问只限制最多选择 个交易日。
- 将香料按收益从大到小扫描,把当前香料放在不晚于 的最晚空闲日。若不存在这样的日期,就舍弃当前香料。
- 依次成功放入的香料构成序列 。前 份就是所有可行的 份香料中收益最大的方案,因此记录收益前缀即可回答全部询问。
【Recall 正确性】
- 把当前香料安排到最晚空位不会占用更早的日期,所以不会使之后失香时间更早的香料更难加入。
- 固定一个收益下界,只看收益不低于它的香料。上述过程会选出其中数量最多的可行集合,否则可以把遗漏的香料加入并反复交换到更晚的位置,与当前香料无法放入矛盾。
- 因此前 个成功单位的第 大收益不会小于任意可行 元方案的第 大收益。对每个位置分别比较,便得到答案前缀的最优性。
【部分分:测试点 1∼41\sim 41∼4】
- 总库存不超过 。可以枚举每个单位香料是否售出,再枚举或搜索这些香料被安排到哪些交易位置。
- 若某个单位被安排在失香时间之后,或者某一天使用了超过 个交易位置,这个方案不合法。同一种香料首次售出时再加入一次初售赏金。
- 时间复杂度为指数级。这一档已经完整保留了截止时间、每日容量和只出现一次的额外收益。
【部分分:测试点 5∼85\sim 85∼8】
-
先推广每天的交易次数。把第 天拆成编号为 的 个位置,那么在第 天失香的单位,其截止位置就是 。
-
再将每种香料展开成 个单位。若 ,第 个单位的失香时间为 ;若 ,它在当前询问内始终有效。
-
展开以后,每个单位仍然只有“收益”和“截止位置”两个属性,这已经与 Recall 中的问题完全相同。
-
还需要处理初售赏金。对第 种香料,取失香时间最晚的一个单位,把它的收益设为 ,其余单位的收益均设为 。
-
若某个方案售出了这种香料,却没有售出这个单位,就用它替换任意一个已经售出的同类单位。收益不变,截止时间只会变晚,所以方案仍然合法。
-
于是初售赏金也被转化成了一个普通单位的收益。对每次询问重新运行 Recall 中的贪心,设总库存为 ,时间复杂度为 。
【部分分:测试点 9∼129\sim 129∼12】
- 令 ,只对前 天建立 个位置,并运行一次推广后的贪心。每成功放入一个单位,就记录当前收益前缀。
- 对询问 ,只需要前 个成功单位。它们原本已经能够合法安排,把所占位置依次向左压缩后,一定可以全部放入前 个位置。
- 因此询问仍然只是读取一个答案前缀。时间复杂度为 ,与原题相比仅仅把可用位置数从 改成了 。
【部分分:测试点 13∼1513\sim 1513∼15】
- 这一档中 。无论库存有多大,答案中真正能够出现的单位都不会超过 个,所以没有必要展开其余单位。
- 同一种香料的普通单位收益均为 ,并且可以按照失香时间分成若干批。我们只记录当前失香时间最晚的一批,整批处理完成后再生成前一批。
- 用优先队列维护各香料当前能够生成的最高收益,用并查集寻找最晚空位。这样恰好是在不改变 Recall 贪心顺序的前提下,延迟生成它真正访问到的单位。
【部分分:测试点 16∼1916\sim 1916∼19】
- 特殊性质保证所有 ,所以全部单位都可以在任意交易日售出,不再需要处理截止位置。
- 每种香料提供一个收益为 的单位,以及至多 个收益为 的单位。把这些带数量的收益段从大到小排序并求前缀即可。
- 时间复杂度为 。这一档说明普通单位可以整批保存,完整数据只需要为每一批重新带上截止位置。
【正解】
- 令 。一天拆成 个位置后,截止第 天对应的位置上界为 ,我们只需要维护前 个位置。
- 若 ,第 种香料在询问范围内的最晚截止日为
当 时,令 。
-
先生成一个截止日为 、收益为 的单位。自然截止日超过 的普通单位合并到第 天,其余普通单位按照相同截止日组成批次。
-
优先队列中只保存每种香料当前应该处理的单位或批次,并按照收益从大到小取出。初售单位的收益不小于同类普通单位,所以尚未生成的单位不可能越过它提前出现。
-
对截止日为 的单位,用并查集查询不超过 的最晚空位。占用位置 后,将它指向 左侧最近的空位,这与 Recall 中的实现完全相同。
-
当前批次全部放入后,才生成这种香料截止时间更早的下一批。这样优先队列始终给出了逐单位展开后,下一个应该被贪心扫描的收益。
-
若一个批次只能放入一部分,最终一定是因为不超过其截止位置的空位已经全部占满。后续同类批次的截止时间只会更早,所以它们也不可能再被放入。
-
每成功放入一个单位,就记录一次收益前缀。设最终成功放入 个单位,询问 的答案就是前 项收益之和。
-
到这里,算法仍然是 aroma 的“收益降序、占用最晚空位、记录答案前缀”,只是用批次与优先队列避免了完整展开库存。
-
优先队列初始加入 个初售单位,之后至多有 个单位真正占用交易位置,因此总处理规模为 。
-
令 。总时间复杂度为 ,空间复杂度为 。
-
收益前缀可能达到 量级,需要使用
long long。多组数据之间还要清空优先队列,并重新初始化前 个并查集位置。
【参考代码】
/*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;}