P1037 重彩书遗年 官方题解
Gioush OJ · P1037 重彩书遗年
重彩书遗年
【题意简述】
每次选择一个长度恰为 的区间,并从左到右依次写入 。一个序列合法,当且仅当它可以通过若干次操作得到,并且每个位置都至少被覆盖一次。求将给定序列修改为合法序列所需的最少修改次数。
【Hint】
【提示】
考虑一个合法序列从左向右的变化。若当前值为 ,它只能接在 后,或者接在某个值为 的位置后。把“是否可行”改成“最多保留多少个原值”,即可得到动态规划。
【数据点 1∼61\sim 61∼6】
合法序列的第一个位置必须为 ,最后一个位置必须为 。
若前一位不是 ,当前值只能接着增加到前一位加 ,或者从 开始一次新的覆写。若前一位是 ,覆盖到前一位的操作已经结束,可以调整操作顺序,使当前位置接在任意合法前缀之后。
枚举保留不修改的位置集合。令 表示处理到第 位,当前位置最终为 ,并且集合中不超过 的位置均保持原值时是否可行。
令 表示第 位不要求保留,或者 。初值为 ,其余状态为假。转移为
最终要求 为真。枚举全部 个集合,取可行集合大小的最大值。
【数据点 7∼127\sim 127∼12】
根据上一档部分分的启发,不再枚举保留集合,而是把状态改成最大保留位置数量。
定义 表示处理到第 位,当前位置最终为 时,最多有多少个位置不需要修改。不可能的状态设为负无穷,初值为
当 时只有常数个状态,直接按照上一档的两种衔接更新。答案为 ,时间复杂度为 。
【数据点 13∼1913\sim 1913∼19】
去掉 的限制后,完整转移为
滚动数组后,时间复杂度为 ,空间复杂度为 。
【转移的充要性】
上述局部转移等价于把合法序列划分为若干连续上升段。相邻两段之间,要么前一段以 结尾,要么后一段从 开始。
先证明必要性。考虑相邻两段分别由两次覆写留下。若左侧覆写较晚,它会继续向右写入,直到留下段尾的 ;若右侧覆写较晚,它会从自身区间的第一个位置留下 。因此每个分段位置必然满足这两个条件之一。
再证明充分性。从左向右按照转移构造。段内使用 ,新段从 开始;当一段以 结束时,对应覆写已经结束,之后可以接入任意合法状态。于是每个满足上述分段条件的序列都可以由若干次覆写得到。
因此,DP 枚举的恰好是全部合法序列。
【正解】
朴素转移中反复出现四类操作:
- 所有状态整体右移一位;
- 查询全部状态的最大值;
- 对 执行 ;
- 对 加 。
使用线段树维护这些状态。处理第 位之前,令线段树坐标 保存 。
先查询旧的 ,记为 Last,再令 。这样旧的 自动落到新的 所在坐标,完成全部状态的整体右移。
新的 等于旧状态最大值 Best,因此在坐标 单点赋值。对 统一执行
对应在线段树上进行一次区间取最大值。最后在坐标 单点加 ,并用返回值更新 Best。
线段树维护区间最大值,支持区间取最大值与单点加。懒标记 Tag 表示整段至少取到的下界,两个标记的复合为取最大值。
初始时全部位置为负无穷,只在表示 的位置写入 。处理完成后,查询表示 的位置,答案为 。
【复杂度分析】
每个位置进行常数次线段树操作。时间复杂度为 ,空间复杂度为 。
【参考代码】
/*Author:EhundateghDate:2026/6/18Name:F.cppYou steal,I kill.*/#include <cstdio>#include <cstring>#include <algorithm>#define LSon Node[Now].LeftSon#define RSon Node[Now].RightSonusing namespace std; int T,n,m,Line[500010],cnt=0; struct node { int l,r; int LeftSon,RightSon; int Max,Tag,Add;}Node[4000010]; void Update(int Now) { Node[Now].Max=max(Node[LSon].Max,Node[RSon].Max);return;} void PushDown(int Now) { if (Node[Now].Tag==-1) return; Node[LSon].Max=max(Node[Now].Tag,Node[LSon].Max); Node[RSon].Max=max(Node[Now].Tag,Node[RSon].Max); Node[LSon].Tag=max(Node[LSon].Tag,Node[Now].Tag); Node[RSon].Tag=max(Node[RSon].Tag,Node[Now].Tag); Node[Now].Tag=-1;return;} inline int Build(int l,int r) { int Now=++cnt; Node[Now]={l,r,0,0,-0x3f3f3f3f,-1,0}; if (l==r) return Now; int Mid=(l+r)>>1; LSon=Build(l,Mid);RSon=Build(Mid+1,r);Update(Now); return Now;} inline void Modify(int Now,int l,int r,int Val) { if (l>r) return; if (Node[Now].l>r||Node[Now].r<l) return; else if (Node[Now].l>=l&&Node[Now].r<=r) Node[Now].Tag=max(Node[Now].Tag,Val),Node[Now].Max=max(Node[Now].Max,Val); else { PushDown(Now); Modify(LSon,l,r,Val);Modify(RSon,l,r,Val);Update(Now); return; }} inline int Add(int Now,int Pos) { int Ret; if (Node[Now].l==Node[Now].r) {Node[Now].Max++;return Node[Now].Max;} else { PushDown(Now); if (Node[LSon].r>=Pos) Ret=Add(LSon,Pos); else Ret=Add(RSon,Pos); Update(Now); } return Ret;} inline int Query(int Now,int l,int r) { if (l>r) return -0x3f3f3f3f; if (Node[Now].l>r||Node[Now].r<l) return -0x3f3f3f3f; else if (Node[Now].l>=l&&Node[Now].r<=r) return Node[Now].Max; else { PushDown(Now);Update(Now); return max(Query(LSon,l,r),Query(RSon,l,r)); }} void Solve() { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++) { scanf("%d",&Line[i]); } int nl=n;cnt=0; Build(1,n+m+10); Modify(1,nl,nl,(Line[1]==1)); int Best=(Line[1]==1); for (int i=2;i<=n;i++) { int Last=Query(1,nl+m-1,nl+m-1); nl--; Modify(1,nl,nl,Best); Modify(1,nl+1,nl+m-1,Last); int NewVal=Add(1,nl-1+Line[i]); Best=max(Best,NewVal); } printf("%d\n",n-Query(1,nl+m-1,nl+m-1)); return ;} int main() { int c; scanf("%d%d",&c,&T); while (T-->0) Solve(); return 0;}