CF1984H Tower Capturing
一类组合贡献怎么算
问题1:\(\{n_1 \times a_1, n_2 \times a_2, \cdots, n_k \times a_k \}\) 的 \(k\) 组合。
不难证明是 \(\binom{n}{n_1, n_2, \cdots, n_k} = \frac{n!}{n_1! n_2! \cdots n_k!}\) 。
因为 \(n_1\) 个 \(a_1\) 的地位是等价的,不论 \(a_1\) 最终的顺序如何,我们只会在最后给它钦定一个自然顺序 \(1, 2, \cdots, a_1\) 。
\(a_2, a_3, \cdots, a_k\) 和 \(a_1\) 一样。
扩展:如果 \(a_1\) 在确定之后,有 \(f(1)\) 种钦定的顺序,\(a_2\) 在确定之后,有 \(f(2)\) 种钦定的顺序……,\(a_k\) 在确定之后,有 \(f(k)\) 种钦定的顺序。
我们可以先把 \(a_1, a_2, \cdots, a_k\) 最终会钦定哪种顺序确定下来,然后他们最后依然是只有一种顺序,组合的贡献依然是 \(\binom{n}{n_1, n_2, \cdots, n_k}\) 。但是确定顺序的方式有很多种,实际上确定顺序的方式有 \(\prod_{i=1}^{k} f_i = f(1) \times f(2) \times \cdots f(k)\) 种。
问题2: 一棵有根树(无向),存在按某种规则对子树标号的方案。如何求这棵树的在该规则下的所有标号方案的数量?
先不管标号方式,只管从叶子向上归纳。设一棵子树根为 \(u\) ,它的儿子集是 \(son(u)\) ,归纳假设 \(v \in son(u)\) 的标号方案数为 \(f(v)\) 。可以先确定每个儿子具体用了那种标号方式,于是每个儿子确定的一棵子树的节点地位就等价了,但不同树之间不等价。在这种情况下, \(u\) 的儿子能够贡献的方案数为 \(\binom{sz(u)-1}{\prod_{v \in son(u)} sz(v)} = \frac{(sz(u)-1)!}{\prod_{v \in son(u)} sz(v)!} = \frac{(sz(u)-1)!}{sz(v_1)! sz(v_2)! \dots sz(v_{|son(u)|})!}\) 。但注意确定标号方式的方案数有 \(\prod_{v \in son(u)} f(v) = f(1) \times f(2) \times \cdots \times f(|son(u)|)\) 种。整理一下,如果在该规则下存在对 \(u\) 的合法转移,那么这个转移是 \((sz(u) - 1)! \prod_{v \in son(u)} \frac{f(v)}{sz(v)!}\)
凸多边形的三角剖分
简单来讲,凸多边形的三角剖分,即选择凸多边形删除一个三角形满足其三个顶点都在凸包上,凸多边形会被剖分成 \(0 \sim 3\) 个新凸多边形
根据一些小分析,三角剖分的三角形数量不会超过凸包的点的数量。
本题存在唯一的三角剖分。
题目保证没有两个点会退化成点,没有三个点会退化成线,这不是很关键,只是避免了无谓的判重判退化处理。
题目保证没有四个点共圆,这意味着任意三个点能唯一确定一个圆。这能确保题目是可做的。我们确定了 \(A,B\) ,将一个很大的圆逐渐缩小,卡住 \(A,B\) 两个点,继续缩小,卡住第三个点,这个点是唯一的。
当两点 \(A,B\) 确定的一条边选定,选择第三个点 \(C\) 时,不存在同样的 \(\angle ACB\) ,否则他们是同一个圆上的圆周角。圆周角越小,圆心角越小,圆弧越扁,能确定的圆越大。
我们可以枚举每个点通过维护 \(\angle ACB\) 最小的点找到可能的 \(C\) ,而不需要枚举完每个点都判断一次是否 \(ABC\) 确定的圆能覆盖所有点。只需在最后判断一次,最可能的点 \(C\) 是不是真的是对的。
本题要根据三角形对凸多边形的剖分顺序建树。即钦定一个三角形为根,剖分掉当前凸包后形成若干个小凸包,每个小凸包中选择的三角形作为已钦定的三角形的儿子。
本题需要先选择三角形的一条边,朝两个半平面扩展剖分。如果两个方向都能扩展,我们无法确定初始的两个三角形谁是谁的儿子。所以存在这种情况,我们讨论两种可能,一号三角形是二号三角形的儿子,二号三角形是一号三角形的儿子。一个问题的所有形式,贡献合并基于加法原理。一个问题的所有划分,贡献合并基于乘法原理。这里显然是前者。
有三个几何的实现需求。
实现点1:能求出凸包。
可以按字典序排序,然后上下凸壳利用 \(cross0p(\overline{\alpha},\overline{\beta},\overline{\gamma})\) 分别求一次,合并成凸包。
实现点2:已知 \(ABC\) ,维护最小的 \(\angle ACB\) 。
最好可以对 \(ABC\) 确定极角序,这样可以让 \(C\) 在线段 \(AB\) 的逆时针方向(如果不是,不妨交换 \(A,B\) )。求点积 dot(CA,CB),和 \(\cos \angle ACB\) 成正比,在 \((0, \pi)\) 内和 \(\angle ACB\) 具有相反单调性。于是只需要维护最大的点积。
- \(\vec{a} \cdot \vec{b} = |\vec{a}| |\vec{b}| \cos \theta \ s.t. \ \theta \in [0,\pi / 2]\) ,有 \(\frac{\vec{a} \cdot \vec{b}}{|\vec{a}| |\vec{b}|} = \cos \theta \ s.t. \ \theta \in [0, \pi / 2]\) ,有 \(|\frac{\vec{a} \cdot \vec{b}}{|\vec{a}| |\vec{b}|}| = \cos \theta \ s.t. \ \theta \in [0, \pi]\) 。于是 \(|\frac{\vec{a} \cdot \vec{b}}{|\vec{a}| |\vec{b}|}|\) 越大,\(\sin \theta \ s.t. \ \theta \in [0, \pi]\) 越小。看起来维护 \(|\frac{\vec{a} \cdot \vec{b}}{|\vec{a}| |\vec{b}|}|\) 就可以了?是的吗?
- 除法一定炸精度。维护 \(\frac{a}{b} > \frac{c}{d}\) 的时候,改成维护一个 pair 表示分子父母,即维护 \(ad>cb\) 。
- 开根一定炸精度。主要是在求两点距离比如 \(|ab|\) 的时候,我们通常能很容易由勾股定理得到 \(|ab|^{2}\) 。继续开根会炸精度。这时候一个做法是在等号或者不等号两边取平方。不改变符号等号或者不等号意义。
实现点3:已知 \(ABC\) ,求 \(ABC\) 确定的圆心。
- 然后可以知道圆心和半径,不难判断是否某个点都在该圆内。如果已知圆心和半径,incircle 或许不算一个几何问题。
- 根据中垂线定理,可以任选两条边,找中点的法向量,求直线交点找到圆心。让圆心对任意三角形上任意一点求距离,就是半径。可能有精度问题,但是简单合理。(再说一遍这个方法精度很坏,区域赛遇到要求外心的且点按浮点形给出的几何题目,不要乱搞。)
- 这里又有一个新的子问题,求两条直线交点。我们可以基于等分线定理,推出 \(p_1,p_2,q_1,q_2\) 直线的交点是 \(p_1 \frac{s_2}{s_1+s_2}, p_2 \frac{s_1}{s_1+s_2}\) ,面积用叉积求,但注意叉积求出的面积带符号。由于 \(s_1\) 和 \(s_2\) 是两个方向的面积,对 \(s_2\) 面积取负,可以保证 \(s_1\) 和 \(s_2\) 符号一样。
注意点:
- 实际上可以三分 eps ,-6,-9,-12,-15 。罚时不是很关键。题目给的点是整点最好宽松一些,给的点是浮点最好紧致一些。
- 这里我用了精度比较差的中垂线定理求圆心,宽松一些的 -6 eps 才过题
- 通常情况下紧致一些会更好,但是判是否在圆内这种带解析计算的,宽松一些可能会更好。
一点结论和观察
一个点集,可以被一个圆覆盖,当且仅当它的凸包可以被这个圆覆盖。
- 证明显然。
一个点集上的三个点确定的圆,可以覆盖这个点集。当且仅当这三个点在凸包上。
- 如果不是,圆一定会分割凸包。
- 否则,让一个相对大的包含凸包的圆缩小,最终总能卡住三个点并且无法继续缩小(如果不存在三点共圆)。
实际问题
给一个点集,不存在两点退化成点,不存在三点退化成线,没有三点共圆。
开始允许操作两个点,其他点不可操作。每次操作,在允许操作的所有点中选择两个,在不允许操作的点中选择一个,要求三点确定的三角形包含初始点集,然后三角形覆盖的所有点加入可操作点集。
询问有多少选择点的方式,使最终所有点都变成可操作的。
思考:
上面已经分析过了,只能在凸包上选点,由于不存在三点共圆,所以操作方式唯一,经过一些归纳发现这个操作等于对凸包三角剖分。且剖分方式唯一,但顺序不唯一。
对三角剖分的方式建树,方式唯一,问题变成了对剖分顺序的方案组合计数。
- 如果剖分不能进行,则是无解。
- 如果剖分只能对一个半平面进行,则能得到一棵剖分树,则是朴素树上 dp 。
- 但开始给定的一条边可以向两个半平面剖分,如果两边的剖分都存在,能得到两棵剖分树,不能直接确定先对哪个半平面进行的剖分,但是可以枚举它的情况,只有两种。要么让第一棵树作为第二棵树根的儿子,要么让第二棵树作为第一棵树根的儿子,对两棵合并的新树做 dp ,对答案的组合贡献按加法合并。
代码:
view
#include <bits/stdc++.h>
using namespace std;
typedef long long i64;
typedef long double db;
#define L(i,a,b) for(int i=(a);i<=(b);i++)
#define R(i,a,b) for(int i=(a);i>=(b);--i)
const int MAXN = 200005;
const int MOD = 998244353;
const long double EPS = 1E-6;
template <class T> inline int sgn(T x) { return x < -(T)EPS ? -1 : (x > (T)EPS); }
template <class T> inline int cmp(T x, T y) { return sgn(x - y); }
template<class T> struct Point {
typedef Point P;
T x, y; // x^2 + y^2 <= sizeof T
explicit Point (T _x = 0, T _y = 0) : x{_x}, y{_y} {}
bool operator < (P p) const { return cmp(x, p.x) == 0 ? cmp(y, p.y) < 0 : cmp(x, p.x) < 0; }
bool operator == (P p) const { return cmp(x, p.x) == 0 && cmp(y, p.y) == 0; }
P operator + (P p) const { return P(x + p.x, y + p.y); }
P operator - (P p) const { return P(x - p.x, y - p.y); }
P operator * (T d) const { return P(x * d, y * d); }
P operator / (T d) const { return P(x / d, y / d); }
T norm2() const { return x * x + y * y; }
T norm() const { return std::sqrt(norm2());}
db disTo2(const P &o) const { return (x-o.x)*(x-o.x)+(y-o.y)*(y-o.y); }
db disTo(const P &o) const { return std::sqrt(disTo2(o)); }
P rot90() const { return P(-y,x); }
T dot(const P &o) const { return x*x+o.y*o.y; };
T det(const P &o) const { return x*o.y-o.x*y; }
};
template<class T> db dot(Point<T> a, Point<T> b) { return a.x * b.x + a.y * b.y; }
template<class T> db det(Point<T> a, Point<T> b) { return a.x * b.y - b.x * a.y; }
typedef Point<db> P;
#define cross(a,b,c) (b-a).det(c-a)
#define crossOp(a,b,c) sgn(cross(a,b,c))
vector<P> convexHull(vector<P> pts) {
if (pts.size() <= 1) return pts;
sort(pts.begin(), pts.end()); int n=pts.size();
vector<P> h(n*2); int k=0,top=0;
L(i,0,n-1){
while(top>1&&crossOp(h[top-2],h[top-1],pts[i])<=0)top--;
h[top++]=pts[i];
}
k=top;
R(i,n-1,0){
while(top>k&&crossOp(h[top-2],h[top-1],pts[i])<=0)top--;
h[top++]=pts[i];
}
h.resize(top-1);
return h;
}
int n,cur;
db x,y;
i64 inv[MAXN],fac[MAXN],ifac[MAXN],dp[MAXN];
int sz[MAXN];
P isLL(P p1,P p2,P q1,P q2){
db a1=cross(p1,p2,q1),a2=-cross(p1,p2,q2);
return (q1*a2+q2*a1)/(a1+a2);
}
bool inCircle(P a, P b, P c, P d) {
// o,r
P m1=(a+b)/2,m2=(a+c)/2;
P v1=m1+(a-b).rot90(),v2=m2+(a-c).rot90();
P o=isLL(m1,v1,m2,v2);
return cmp(o.disTo2(d),o.disTo2(a))<=0;
}
void solve() {
cin >> n;
vector<P> pts(n);
L(i,0,n-1){
cin>>x>>y;
pts[i]=P(x,y);
}
vector<P> v=convexHull(pts); n = v.size();
int pit1=-1,pit2=-1;
L(i,0,n-1)if(cmp<db>(pts[0].x,v[i].x)==0&&cmp<db>(pts[0].y,v[i].y)==0)pit1=i;
L(i,0,n-1)if(cmp<db>(pts[1].x,v[i].x)==0&&cmp<db>(pts[1].y,v[i].y)==0)pit2=i;
if (pit1==-1||pit2==-1) { cout<<0<<"\n"; return; }
if (v.size() <= 3) { cout<<1<<"\n"; return; }
queue<array<int, 3> > q;
vector<vector<int> > son(n + 1);
q.push({pit1, pit2, -1});
q.push({pit2, pit1, -1});
auto find=[&](int l,int r)->int{
int st=l,ed=r; if(ed<st)ed+=n; // l,l+1,...,n,1,2,...r -> l,l+1,...,n,n+1,n+2,...,n+r
std::pair<i64, i64> mx={-2,1}; int loc=-1;
L(i,st+1,ed-1){ // get w
P ba=v[l]-v[i%n],bc=v[r]-v[i%n]; i64 o=dot(ba,bc);
if( cmp(1.L*mx.first*ba.norm2()*bc.norm2(),1.L*(sgn(o)*o*o)*mx.second)<0){
mx={sgn(o)*o*o,ba.norm2()*bc.norm2()},loc=i%n;
}
}
if(loc==-1) return -1; // the Angle is leaf
for(P Q:v)if(!inCircle(v[l],v[loc],v[r],Q))return -2; // the root is bad
return loc;
};
cur=1;
while(!q.empty()){
auto p = q.front(); q.pop();
int x=find(p[0], p[1]);
if(x==-2){ std::cout<<0<<"\n"; return; }
if(x==-1)continue;
q.push({p[0],x,cur}); q.push({x,p[1],cur});
if(p[2]!=-1)son[p[2]].push_back(cur);
cur++;
}
function<int(int)>dfs=[&](int u)->int{
if (dp[u]!=-1LL) {return dp[u];}
dp[u]=1LL;
sz[u]=1;
if(!son[u].empty()){
for(auto v:son[u]){
dfs(v);
sz[u]+=sz[v];
dp[u]=(dp[u]*dp[v]%MOD*ifac[sz[v]])%MOD;
}
dp[u]=(dp[u]*fac[sz[u]-1])%MOD;
}
return dp[u];
};
i64 res = 0;
if(std::find(son[1].begin(),son[1].end(),2)!=son[1].end()){
L(i,1,n)dp[i]=-1,sz[i]=-1;
res = dfs(1);
}else{
L(i,1,n)dp[i]=-1,sz[i]=-1; son[1].push_back(2);
dfs(1); res=dp[1];
son[1].pop_back();
L(i,1,n)dp[i]=-1,sz[i]=-1; son[2].push_back(1);
dfs(2); res=(res+dp[2])%MOD;
}
cout<<res<<"\n";
}
signed main() {
cin.tie(0)->ios::sync_with_stdio(false);
inv[1]=1; L(i,2,MAXN-1)inv[i]=((MOD-MOD/i)*inv[MOD%i])%MOD;
fac[0]=fac[1]=ifac[0]=ifac[1]=1;
L(i,2,MAXN-1)fac[i]=(fac[i-1]*i)%MOD,ifac[i]=(ifac[i-1]*inv[i])%MOD;
int _=1;
cin>>_;
while(_--){ solve(); }
}
浙公网安备 33010602011771号