1 solutions
-
0
P1011 猫猫的积木块
问题转化
初始有 个格子,每次合并操作会减少一个格子,因此 最多合并次数 。
问题等价于:将原序列划分成最少的段,使得每一段内的所有积木块能够通过若干次合并操作(按题目规则)最终合并成一个格子。
判断一段能否合并成一个
给定一个子数组 ,如何判断它能否完全合并成一个?
观察合并规则:两个相邻格子能合并当且仅当它们的值域区间不重叠,即一个的最大值小于另一个的最小值,或反之。
进一步分析发现,若一个格子内包含多个积木,它的值域是这些积木的最小值和最大值。在最优合并策略下,我们总是优先合并值域相邻的两个格子,因为如果中间有空隙,合并后会产生“空洞”,这个空洞会阻止后续落入其中的积木与之合并,最终导致无法全部合并。因此,能合并成一个的充要条件是:可以通过不断合并值域恰好相邻的两个格子,最终变成一个。这启发我们设计一个线性模拟算法(离散化后):
- 将区间内所有数按值排序,得到它们的排名( 到 )。
- 按照原始顺序,每个位置上的数对应一个排名,初始时每个格子只包含一个数,所以该格子的最小排名 最大排名 这个排名。
- 用双向链表维护相邻关系,反复扫描:
- 检查相邻两格,如果它们的值域区间恰好相邻(即一个的最大排名与另一个的最小排名相差 ),就将它们合并。
- 合并后,新区间的最小排名为两者最小值,最大排名为两者最大值,并更新链表。
- 最终若所有格子合并成一个,则返回
true,否则返回false。
该算法的时间复杂度为 ,主要来自排序。
贪心分段
我们要将整个序列划分成尽可能少的段,每个段都能完全合并。
显然,从左到右贪心是最优的:每次从当前位置 出发,找到最远的 使得 可合并,然后令 继续。现在问题变为:给定左端点 ,如何快速找到最大的 ?
朴素二分
直接对 进行二分,每次调用一次 的
check函数。
但这样总复杂度可能达到 ,不可接受。倍增 + 二分
经典的优化技巧:先通过倍增确定一个合适的二分上界,再在区间内二分。
具体流程(设当前左端点为 ,当前已确定的可合并右端点 初始为 ,步长 ):
- 尝试扩展 个元素:若 可合并,则令 ,,继续尝试。
- 否则,将 减半,继续尝试(直到 停止)。
- 最终得到的 就是当前段的最远右端点。
这样做,每个元素被检查的次数为 ,每次
check需要 时间,但所有检查的区间长度之和是 级别(因为每次倍增都会产生新的区间,总长度受倍增过程控制)。更精确的分析表明总复杂度为 ,在 时可接受。算法步骤总结
- 读入 组数据,每组数据有 和排列 。
- 初始化 (‑based),段数
cnt = 0。 - 当 时:
- 令 ,。
- 先将当前段的首个元素放入临时数组
tc(tc[0] = a[l])。 - 倍增扩展:
- 当 且
sol(l, r, k)为真时,执行 ,。 - 否则,不断将 减半,直到 ,并在过程中如果
sol(l, r, k)为真则更新 。
- 当 且
- 此时 为当前段能合并的最后一个位置,
cnt++,令 。
- 输出 。
关键函数
sol(l, r, nw)判断区间 (已确认可合并)加上后面 个元素 能否整体合并成一个。
实现细节:
- 已知当前段 内部已经合并成了有序序列
ta(在之前的调用中已维护在tc中),新部分tb为 。 - 将
ta拷贝出来(memcpy(ta, tc, (r-l+1)*4)),将tb排序,然后将ta和tb归并到tc中,得到整个区间所有数的有序列表。 - 建立排名:
rk[tc[i]] = i。 - 初始化链表:对于 ,位置 对应原始顺序中的 ,其
li[i]=ri[i]=rk[a[l+i]],前驱prv[i]=i-1,后继nxt[i]=i+1。注意链表数组大小比实际多一位(用于哨兵)。 - 模拟合并:
- 从第一个格子
cur=0开始。 - 向右合并:当
nxt[cur] < pc且li[cur] - ri[nxt[cur]] == 1或li[nxt[cur]] - ri[cur] == 1时,调用mer(cur, nxt[cur])合并。 - 向左合并:当
prv[cur] >= 0且满足相邻条件时,调用mer(prv[cur], cur)并将cur指向合并后的节点(cur = prv[cur])。 - 然后
cur = nxt[cur]继续扫描。
- 从第一个格子
- 重复直到
cur >= pc。 - 判断是否只剩一个格子:检查
prv[cur] == 0(cur此时是链表末尾的哨兵,其前驱为 表示链表中只有节点 )。 - 若成功返回 ,否则恢复
tc为原来的ta并返回 。
复杂度分析
- 每个元素在倍增过程中被检查 次,每次检查需要排序和模拟,模拟部分线性。
- 排序的总复杂度:每个元素参与归并的次数也是 ,每次归并的代价与其所在段长度有关,但总复杂度为 。由于所有测试数据的 总和不超过 ,该复杂度足以通过。
参考代码
#include<bits/stdc++.h> using namespace std; const int N=200005; const int P=998244353; int t,n,m; int a[N],b[N]; int ta[N],tb[N],tc[N]; int rk[N]; int li[N],ri[N],prv[N],nxt[N]; void mer(int x,int y){ li[x]=min(li[x],li[y]); ri[x]=max(ri[x],ri[y]); nxt[x]=nxt[y]; prv[nxt[x]]=x; } bool sol(int l,int r,int nw){ memcpy(ta,tc,(r-l+1)*4); for(int i=0;i<nw;i++){ tb[i]=a[i+r+1]; } sort(tb,tb+nw); int pa=0,pb=0,pc=0; int na=r-l+1,nb=nw; while(pa<na&&pb<nb){ if(ta[pa]<tb[pb])tc[pc++]=ta[pa++]; else tc[pc++]=tb[pb++]; } while(pa<na)tc[pc++]=ta[pa++]; while(pb<nb)tc[pc++]=tb[pb++]; for(int i=0;i<pc;i++)rk[tc[i]]=i; for(int i=0;i<=pc;i++){ li[i]=ri[i]=rk[a[l+i]]; prv[i]=i-1;nxt[i]=i+1; } int cur=0; while(cur<pc){ while(nxt[cur]<pc){ int pt=nxt[cur]; if(li[cur]-ri[pt]==1||li[pt]-ri[cur]==1){ mer(cur,pt); }else break; } while(prv[cur]>=0){ int pt=prv[cur]; if(li[cur]-ri[pt]==1||li[pt]-ri[cur]==1){ mer(pt,cur); cur=pt; }else break; } cur=nxt[cur]; } if(prv[cur]==0)return 1; else{ memcpy(tc,ta,(r-l+1)*4); return 0; } } int main(){ scanf("%d",&t); while(t--){ scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%d",a+i); } int l=0,r=0,k=1,cnt=0; while(l<n){ tc[0]=a[l]; while(r+k<n&&sol(l,r,k)){ r+=k; k*=2; } while(r+k>=n)k/=2; while(k){ if(sol(l,r,k))r+=k; k/=2; } cnt++; l=r+1;r=l;k=1; } printf("%d\n",n-cnt); } return 0; }总结
本题的核心在于:
- 将最多合并次数转化为最少段数。
- 设计一个高效的区间可合并性判定算法(离散化 链表模拟)。
- 利用倍增 二分快速找出每个段的最大右端点,避免 的复杂度。
总时间复杂度 ,空间复杂度 ,可以顺利通过所有测试数据。
Information
- ID
- 12
- Time
- 1000ms
- Memory
- 1024MiB
- Difficulty
- 10
- Tags
- # Submissions
- 2
- Accepted
- 1
- Uploaded By