【洛谷5288】[HNOI2019] 多边形(二叉树模型)
大致题意: 给你一个多边形,用若干不重合、不相交的线段将其划分为若干三角形区域,并定义旋转操作\((a,c)\)为选定\(4\)个点\(a,b,c,d\)满足\(a<b<c<d\)且\(ab,ac,ad,bc,cd\)有连边,然后删去边\(ac\)并加上边\(bd\)。带修独立询问至少旋转几次使得无法继续旋转以及方案数。
关于无法继续旋转
第一次看完题面,我是一脸懵逼:选中四个点不可以无限重复旋转吗?为什么会无法旋转?
然后冷静了一下重新看了遍题面,才发现\(a,b,c,d\)是有序的。。。
再看样例解释可以发现,似乎最终状态就是每个点都向\(n\)号节点连边?(似乎也挺好证明的)
至此才算勉强明白了题目在讲什么。
接下来,我们先考虑没有修改、单组询问怎么做。
处理第一个询问
不难发现,第一个询问还是比较简单的。
首先易知,每一次旋转操作,我们都可以将某条原本并不连向\(n\)的边变成连向\(n\)的边。
那么,根据贪心,有多少条边不与\(n\)相连,答案就是多少。
实现似乎也不是很难?
前置知识:二叉树模型
在处理第二个询问之前,我们需要知道,这题涉及到一个叫“二叉树模型”的东西。
由于我们之前第一个询问确定答案的方式,易知,在一条边连向\(n\)之后,我们就不会再去旋转它了(不然肯定不优)。
又因为题目中给出了边不重复、不相交的条件,且旋转操作是不会使边相交的,因此我们可以发现,每一条连向\(n\)的边,都会把图给分成两部分。
而我们要知道一个结论:在图的每一部分中,有且仅有一条不与\(n\)相连的边可以旋转成与\(n\)相连的边,也就是说每次旋转的边是唯一确定的。
再结合前面提到过的二叉树模型,那么我们就可以考虑,建一棵二叉树,即把当前旋转的边作为父亲,被分成的两部分作为左右儿子。
于是我们就建成了一棵二叉树。
然后考虑对于每个被多边形中原有的边划分出的一部分,我们都需要建一棵二叉树,因此就会建成若干棵二叉树。
利用这些二叉树处理询问就简单了许多。
处理第二个询问
现在我们来考虑如何处理第二个询问。
我们在二叉树上从下往上讨论,对于叶子节点,显然它只有一种旋转方式,即旋转它本身。
否则,对于二叉树中有两个子树大小分别为\(x\)和\(y\)的儿子的节点,我们这样考虑它的方案:
- 首先,子节点的子树内部的顺序,肯定在以该子节点为根时已经计算过了。所以,我们只需考虑子树间的顺序。
- 那么,问题就相当于归并两个长度分别为\(x\)和\(y\)的有序数列的方案数。这是一个经典的组合问题,答案就是\(C_{x+y}^x\)。
- 综上所述,每次合并节点时,我们将方案数乘上\(C_{x+y}^x\)即可。
最后,我们再来考虑下每棵二叉树根节点之间的贡献。
这其实也挺简单的吧,就是在枚举根的同时记录先前枚举过的二叉树的总\(Size\),然后考虑和前面一样的计算方式即可。
这样我们就成功求解了答案
处理多组询问
考虑每次只旋转一条边,且询问独立。
对于第一个询问,我们只要判断旋转的这条边在二叉树中是否有父亲,如果没有,说明旋转它可以向\(n\)连边,因此将答案减\(1\)。否则答案不变。
对于第二个询问,其实也就可以理解为在二叉树上修改一对父子关系,有点类似于\(Treap\)和\(Splay\)中的\(Rotate\)操作吧。
但注意要对于修改的节点是否为一棵二叉树的根进行分类讨论:
如果这个节点不是一棵二叉树的根,首先,我们除去它两个子节点合并的贡献以及它与其兄弟合并的贡献(即其父亲两个子节点合并的贡献)。
要加上的贡献就略复杂了。
若用\(sz_i\)表示以\(i\)为根的子树大小,并记该节点为\(x\),其父节点为\(f\),并且\(x\)是\(f\)的\(d\)儿子(\(0\)为左,\(1\)为右),则要加上的贡献可推得:
我们先将\(x\)原先的\(d\text{^}1\)儿子现在与\(f\)的\(d\text{^}1\)儿子计算贡献,然后再将\(f\)子树总\(Size\)除去\(x\)及其\(d\)儿子\(Size\)后再一同与\(x\)的\(d\)儿子的\(Size\)计算贡献。
\(x\)为根类似,只不过是要考虑其他二叉树的总\(Size\)与\(x\)子节点\(Size\)的合并。
这里的具体实现可见代码。
代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100000
#define X 1000000007
#define swap(x,y) (x^=y^=x^=y)
#define pb push_back
using namespace std;
int n,ty,Fac[N+5],IFac[N+5];vector<int> s[N+5];
class FastIO
{
private:
#define FS 100000
#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
#define pc(c) (C^FS?FO[C++]=c:(fwrite(FO,1,C,stdout),FO[(C=0)++]=c))
#define tn (x<<3)+(x<<1)
#define D isdigit(c=tc())
int T,C;char c,*A,*B,FI[FS],FO[FS],S[FS];
public:
I FastIO() {A=B=FI;}
Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
Tp I void write(Ty x) {W(S[++T]=x%10+48,x/=10);W(T) pc(S[T--]);}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
Tp I void write(Con Ty& x,Con char& y) {write(x),pc(y);}
I void clear() {fwrite(FO,1,C,stdout),C=0;}
}F;
class BinaryTreeSolver//二叉树模型
{
private:
#define LB lower_bound
#define GV(x,y) (1LL*Fac[(x)+(y)]*IFac[x]%X*IFac[y]%X)//求值,就是一个组合数
#define GIV(x,y) (1LL*IFac[(x)+(y)]*Fac[x]%X*Fac[y]%X)//除去贡献,直接用逆元,避免多一个log
#define GV_S(x) GV(O[O[x].S[0]].Sz,O[O[x].S[1]].Sz)//求合并两儿子的值
#define GIV_S(x) GIV(O[O[x].S[0]].Sz,O[O[x].S[1]].Sz)//求合并两儿子的值的逆元
int w,v,g,tot,rt[N+5],sz[N+5],S[N+5][2];struct node {int Sz,F,S[2];}O[N+5];
struct Pr
{
int l,r;I Pr(CI a=0,CI b=0):l(a),r(b){}
I bool operator < (Con Pr& o) Con {return l^o.l?l<o.l:r<o.r;}
};map<Pr,int> p;
I void SU(CI l,CI r,int& rt,CI lst=0)//建立二叉树
{
if(l+2>r) return;RI mid=s[r][LB(s[r].begin(),s[r].end(),l+1)-s[r].begin()];//如果只剩左右端点,返回,否则用lower_bound找到二叉树父节点
O[p[Pr(l,r)]=rt=++tot].F=lst,SU(l,mid,O[rt].S[0],rt),SU(mid,r,O[rt].S[1],rt),
O[rt].Sz=O[O[rt].S[0]].Sz+O[O[rt].S[1]].Sz+1,v=1LL*v*GV_S(rt)%X;//建树
}
public:
I BinaryTreeSolver() {v=1;}
I void Init(CI x,CI l,CI r) {SU(l,r,rt[x]),v=1LL*v*GV(g,O[rt[x]].Sz)%X,g+=O[rt[x]].Sz;}//初始化
I void PWork() {w=n-1-s[n].size(),ty?(F.write(w,' '),F.write(v,'\n')):F.write(w,'\n');}//求初始答案
I void UWork(CI x,CI y)//处理带修独立询问
{
RI k=p[Pr(x,y)],f=O[k].F,d=O[f].S[1]==k;
if(!ty) return F.write(w-(!f),'\n');F.write(w-(!f),' ');//对于无需第二个询问直接输出并退出函数
if(f)//如果不是根
{
RI t=1LL*v*GIV_S(k)%X*GIV_S(f)%X*GV(O[O[f].S[d^1]].Sz,O[O[k].S[d^1]].Sz)%X;
F.write(1LL*t*GV(O[f].Sz-O[k].Sz+O[O[k].S[d^1]].Sz,O[O[k].S[d]].Sz)%X,'\n');
}
else//如果是根
{
RI t=1LL*v*GIV_S(k)%X*GIV(g-O[k].Sz,O[k].Sz)%X*GV(g-O[k].Sz,O[O[k].S[0]].Sz)%X;
F.write(1LL*t*GV(g-O[k].Sz+O[O[k].S[0]].Sz,O[O[k].S[1]].Sz)%X,'\n');
}
}
}T;
I int Qpow(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
int main()
{
RI Qtot,i,x,y;for(F.read(ty,n),i=1;i<=n-3;++i) F.read(x,y),s[x].pb(y),s[y].pb(x);//读入
for(s[n].pb(1),i=1;i^n;++i) s[i].pb(i+1);for(s[1].pb(n),i=2;i<=n;++i) s[i].pb(i-1);//处理边
for(i=1;i<=n;++i) sort(s[i].begin(),s[i].end());//排序
for(Fac[0]=i=1;i<=n;++i) Fac[i]=1LL*Fac[i-1]*i%X;//预处理阶乘
for(IFac[n]=Qpow(Fac[n],X-2),i=n-1;~i;--i) IFac[i]=1LL*IFac[i+1]*(i+1)%X;//预处理阶乘逆元
for(x=s[n].size(),i=0;i^(x-1);++i) T.Init(i,s[n][i],s[n][i+1]);T.PWork();//初始化
F.read(Qtot);W(Qtot--) F.read(x,y),x>y&&swap(x,y),T.UWork(x,y);//求答案
return F.clear(),0;
}