P1071 风铃九重错响 官方题解
Gioush OJ · P1071 风铃九重错响
【部分分:测试点 1∼41\sim 41∼4】
- 装置有 根横杆,每根横杆都有回旋与不回旋两种选择。枚举全部 种选择后,递归得到最终音牌序列。
- 对每个序列枚举所有位置对统计错响,时间复杂度为 。
- 横杆回旋只会改变左右两座子装置的先后关系,不会改变它们各自内部已经产生的错响。这是后续拆分贡献的依据。
【部分分:测试点 5∼95\sim 95∼9】
- 对每座子装置保存其中全部音牌值排好序后的序列。处理一根横杆时,用双指针统计左侧音牌大于右侧音牌的二元组数量。
- 设左右音牌数分别为 ,保持原方向产生 次跨侧错响。由于音牌值互不相同,回旋后产生的跨侧错响恰好为 。
- 当前横杆贡献 ,随后归并两个有序序列。最坏时间复杂度为 。
【部分分:测试点 10∼1410\sim 1410∼14】
- 特殊性质保证每根横杆至少有一侧直接悬挂一枚音牌,因此装置可以看成逐次向已有序列加入一个新值。
- 用树状数组维护已经出现的音牌值。加入 时,前缀和给出已有值中小于 的数量,总数减去前缀和便得到大于 的数量。
- 两个数量分别对应新音牌放在左侧与右侧时的跨侧错响。取较小值加入答案,时间复杂度为 。
【部分分:测试点 15∼1915\sim 1915∼19】
- 这一档只有 ,继续使用第二档的有序序列归并即可。
- 每个结点都能独立取 ,说明动态规划本身已经完成。平方复杂度只来自反复扫描并复制子树中的音牌值。
- 正解需要让所有子树共享同一套值域结构,并在合并结构的同时统计跨侧错响。
【正解】
- 为每个子装置建立一棵值域线段树。叶子 记录这座子装置中值为 的音牌数量,每个内部结点记录对应值域中的音牌总数。
- 合并左树 与右树 时,当前值域中 的右半部分都大于 的左半部分,所以产生
次跨侧错响。随后递归合并两侧相同值域。
-
递归得到的总和就是保持方向时的 。当前横杆答案仍然加入 。
-
定义 表示以结点 为根的装置内部最少错响数。叶子的边界为 。
-
若横杆 的左右儿子为 ,跨侧错响为 ,左右音牌数为 ,完整转移为
-
输入保证儿子编号小于父亲编号,所以按照编号递增处理时,两个儿子的值域树与答案都已经计算完成,不需要再进行递归遍历。
-
每枚音牌最初只在根到叶的一条值域路径上建立结点,合并时直接复用已有结点,不复制整棵树。
-
每层值域中,一个结点只会被并入更大的子装置一次,总时间复杂度为 ,空间复杂度为 。
-
错响数量最大为 ,答案与合并时的乘积都必须使用
long long。每组数据开始前要重置结点计数器与空结点。
【参考代码】
/*Author:EhundateghDate:2026/8/19Name:chime.cppYou steal,I kill.*/#include <cstdio>#include <cstring>#include <algorithm>#define MAXN 400010#define MAXM 4200010using namespace std; int c,T,n,cnt=0,Line[MAXN],ls[MAXN],rs[MAXN],Root[MAXN];long long Ans[MAXN]; struct node{ int ls,rs,Count;}Node[MAXM]; int Insert(int l,int r,int Pos) { int Now=++cnt; Node[Now]={0,0,1}; if (l==r) return Now; int Mid=(l+r)>>1; if (Pos<=Mid) Node[Now].ls=Insert(l,Mid,Pos); else Node[Now].rs=Insert(Mid+1,r,Pos); return Now;} int Merge(int x,int y,int l,int r,long long &Ret) { if (!x||!y) return x+y; if (l==r) { Node[x].Count+=Node[y].Count; return x; } Ret+=1ll*Node[Node[x].rs].Count*Node[Node[y].ls].Count; int Mid=(l+r)>>1; Node[x].ls=Merge(Node[x].ls,Node[y].ls,l,Mid,Ret); Node[x].rs=Merge(Node[x].rs,Node[y].rs,Mid+1,r,Ret); Node[x].Count=Node[Node[x].ls].Count+Node[Node[x].rs].Count; return x;} void Solve() { scanf("%d",&n); cnt=0;Node[0]={0,0,0}; for (int i=1;i<=n;i++) scanf("%d",&Line[i]); for (int i=n+1;i<=2*n-1;i++) scanf("%d%d",&ls[i],&rs[i]); for (int i=1;i<=n;i++) { Root[i]=Insert(1,n,Line[i]); Ans[i]=0; } for (int i=n+1;i<=2*n-1;i++) { long long Ret=0; long long Left=Node[Root[ls[i]]].Count,Right=Node[Root[rs[i]]].Count; Root[i]=Merge(Root[ls[i]],Root[rs[i]],1,n,Ret); Ans[i]=Ans[ls[i]]+Ans[rs[i]]+min(Ret,Left*Right-Ret); } printf("%lld\n",Ans[2*n-1]); return;} int main() { scanf("%d%d",&c,&T); while (T-->0) Solve(); return 0;}