【loj - 3056】「HNOI2019」多边形
description
小 R 与小 W 在玩游戏。
他们有一个边数为 \(n\) 的凸多边形,其顶点沿逆时针方向标号依次为 \(1,2,3,\dots,n\)。最开始凸多边形中有 \(n\) 条线段,即多边形的 \(n\) 条边。这里我们用一个有序数对 \((a,b)\)(其中 \(a<b\))来表示一条端点分别为顶点 \(a,b\) 的线段。
在游戏开始之前,小 W 会进行一些操作。每次操作时,他会选中多边形的两个互异顶点,给它们之间连一条线段,并且所连的线段不会与已存的线段重合、相交(只拥有一个公共端点不算作相交)。他会不断重复这个过程,直到无法继续连线,这样得到了状态 \(S_0\)。\(S_0\) 包含的线段为凸多边形的边与小 W 连上的线段,容易发现这些线段将多边形划分为一个个三角形区域。对于其中任意一个三角形,其三个顶点为 \(i,j,k(i<j<k)\),我们可以给这个三角形一个标号 \(j\),这样一来每个三角形都被标上了 \(2,3,\dots,n-1\) 中的一个,且没有标号相同的两个三角形。
小 W 定义了一种「旋转」操作:对于当前状态,选定 \(4\) 个顶点 \(a,b,c,d\),使其满足 \(1\leq a<b<c<d\leq n\) 且它们两两之间共有 \(5\) 条线段—— \((a,b),(b,c),(c,d),(a,d),(a,c)\),然后删去线段 \((a, c)\),并连上线段 \((b,d)\)。那么用有序数对 \((a,c)\) 即可唯一表示该次「旋转」。我们称这次旋转为 \((a,c)\) 「旋转」。显然每次进行完“旋转”操作后多边形中依然不存在相交的线段。
当小 W 将一个状态作为游戏初始状态展示给小 R 后,游戏开始。游戏过程中,小 R 每次可以对当前的状态进行「旋转」。在进行有限次「旋转」之后,小 R 一定会得到一个状态,此时无法继续进行「旋转」操作,游戏结束。那么将每一次「旋转」所对应的有序数对按操作顺序写下,得到的序列即为该轮游戏的操作方案。
为了加大难度,小 W 以 \(S_0\) 为基础,产生了 \(m\) 个新状态。其中第 \(i\) 个状态 \(S_i\) 为对 \(S_0\) 进行一次「旋转」操作后得到的状态。你需要帮助小 R 求出分别以 \(S_0,S_1,\dots,S_m\) 作为游戏初始状态时,小 R 完成游戏所用的最少「旋转」次数,并根据小 W 的心情,有时还需求出旋转次数最少的不同操作方案数。由于方案数可能很大,输出时请对 \(10^9+7\) 取模。
solution
把三角剖分每条边 \((a, b)\) 看成区间 \((a, b)\)(不妨假设 \(a < b\))。
则根据三角剖分的性质,任意两区间要么相离,要么包含,且存在区间 \((1,2),(2,3)\dots,(n-1,n)\) 与 \((1,n)\)。
考虑建树:区间 \((l, r)\) 的儿子为它所包含的极大区间 \((p, q)\)(不存在区间介于 \((p, q)\) 与 \((l, r)\) 之间)。
那么该树叶子为 \((1,2),(2,3)\dots,(n-1,n)\),根为 \((1,n)\)。
考虑这棵树的特殊性质:不存在儿子个数恰好为 \(1\) 的非叶结点,非叶结点数量为 \(n - 2\)(算上根结点),叶子数量为 \(n - 1\)。
那么显然这棵树是二叉树,且非叶结点的儿子个数恰为 2。
考虑旋转操作对应到树上的含义:右旋非叶结点(真就“旋转”操作啊)。
考虑终止状态的含义:非叶结点形成一条右链。也就是说不存在非叶结点 x 在 y 的左子树内。
我们可以先删掉叶结点。
每次旋转最多会使一个点不在某点左子树中,且总可以旋转最右链上的点使得左子树点减少。
那么最小次数即最右链上的点的左子树大小总和 cnt。
考虑方案数,每次可以任选最右链上某一点进行旋转。通过简单的组合推导可得方案数为:
一次右旋只会影响 \(O(1)\) 个点,简单分类回答询问即可。
accepted code
#include <map>
#include <cstdio>
#include <vector>
#include <cassert>
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int, int> pii;
#define pr make_pair
#define last(v) v[v.size() - 1]
#define all(v) v.begin(), v.end()
#define pb(x) push_back(x)
const int MOD = int(1E9) + 7;
const int MAXN = 100000;
inline int add(int x, int y) {x += y; return x >= MOD ? x - MOD : x;}
inline int sub(int x, int y) {x -= y; return x < 0 ? x + MOD : x;}
inline int mul(int x, int y) {return (int)(1LL * x * y % MOD);}
int fct[MAXN + 5], inv[MAXN + 5];
void init() {
fct[0] = 1; for(int i=1;i<=MAXN;i++) fct[i] = mul(fct[i - 1], i);
inv[1] = 1; for(int i=2;i<=MAXN;i++) inv[i] = sub(0, mul(inv[MOD % i], MOD / i));
}
map<pii, int>mp; int ncnt;
int id(int l, int r) {
if( mp.count(pr(l, r)) ) return mp[pr(l, r)];
else return mp[pr(l, r)] = (++ncnt);
}
vector<int>vl[MAXN + 5], vr[MAXN + 5];
bool rgt[MAXN + 5]; int siz[MAXN + 5], ch[2][MAXN + 5], fa[MAXN + 5];
int build(int l, int r, bool f) {
if( l + 1 == r ) return 0;
int x = id(l, r), m = last(vr[l]);
assert(last(vl[r]) == m);
vr[l].pop_back(), vl[m].pop_back(), fa[ch[0][x] = build(l, m, false)] = x;
vr[m].pop_back(), vl[r].pop_back(), fa[ch[1][x] = build(m, r, f)] = x;
siz[x] = siz[ch[0][x]] + siz[ch[1][x]] + 1, rgt[x] = f;
return x;
}
bool cmp(const int &x, const int &y) {return x > y;}
int main() {
init(); int W, n, m; scanf("%d%d", &W, &n);
for(int i=1;i<n;i++) vr[i].pb(i + 1);
for(int i=n;i>1;i--) vl[i].pb(i - 1);
for(int i=1;i<=n-3;i++) {
int x, y; scanf("%d%d", &x, &y);
vl[x].pb(y), vr[x].pb(y), vl[y].pb(x), vr[y].pb(x);
}
for(int i=1;i<=n;i++)
sort(all(vr[i])), sort(all(vl[i]), cmp);
build(1, n, true);
int ans = 1, cnt = 0;
for(int i=1;i<=ncnt;i++)
if( !rgt[i] ) cnt++, ans = mul(ans, inv[siz[i]]);
if( W == 1 ) printf("%d %d\n", cnt, mul(ans, fct[cnt]));
else printf("%d\n", cnt);
scanf("%d", &m);
for(int i=1;i<=m;i++) {
int l, r; scanf("%d%d", &l, &r);
int x = id(l, r), cnt1, ans1;
if( rgt[fa[x]] )
cnt1 = cnt - 1, ans1 = mul(mul(ans, fct[cnt - 1]), siz[x]);
else {
int p = ch[1][fa[x]], q = ch[1][x];
cnt1 = cnt, ans1 = mul(mul(ans, fct[cnt]), mul(siz[x], inv[siz[p] + siz[q] + 1]));
}
if( W == 1 ) printf("%d %d\n", cnt1, ans1);
else printf("%d\n", cnt1);
}
}
details
当然也可以不用像这样显性地建树。
事实上这道题转化问题的方法比较多,这里只是采用了我使用的一种。
最后代码实现比较简单,对问题一环套一环的分析更有趣些。