zoukankan      html  css  js  c++  java
  • 【洛谷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 ext{^}1)儿子现在与(f)(d ext{^}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,'
    ')):F.write(w,'
    ');}//求初始答案
    		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),'
    ');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,'
    ');
    			}
    			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,'
    ');
    			}
    		}
    }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;
    }
    
  • 相关阅读:
    SQL Server 触发器
    T-SQL查询进阶-10分钟理解游标
    有关T-SQL的10个好习惯
    iOS 画虚线以及drawRect的使用总结:
    iOS一个for循环实现,几行 几列 的布局形式
    IOS 符合某类型的子视图批量从父视图中移除
    DESC 和 ASC
    把数组格式数据转换成字符串存入数据库
    Swift :?和 !
    Swift 类构造器的使用
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/Luogu5288.html
Copyright © 2011-2022 走看看