SRM566 1000pts
绍一的模拟赛题
【题意】
小Z养了$𝑚(\leqslant 50)$只企鹅,第$i$只企鹅的坐标为$(x_i, y_i)$,颜色为$𝑐_𝑖$。出于对企鹅安全的考虑,他决定造栅栏。地面上有$𝑛(\leqslant 200)$个桩,编号$0...𝑛 − 1$,小Z会用一些直的栅栏,每个栅栏连接某两个桩子。对于最后的结果,任意两个栅栏不能交叉。任意一个桩只能连接0个或2个栅栏,栅栏必须围成一些封闭的多边形。由于经费有限,每一个多边形内至少包含一只企鹅。小Z的企鹅有着许多颜色。相同颜色的企鹅必须在同一个多边形内。为了保证所有企鹅的安全,每一只企鹅都必须被包含在某一个多边形内。
各种不合法的情况:
某种合法的情况:
现在小Z想知道自己有多少种合法的不同的造栅栏的方案𝑎𝑛𝑠。
【题解】
用ℎ[𝑙][𝑟][0..1]表示𝑙, 𝑟必须选,点的编号在𝑙, 𝑟之间,最外层的多边形内没有(0)或有(1)企鹅的除了最外层多边形其他多边形均合法的方案数。𝑔[𝑙][𝑟]表示𝑙必须选,点的编号在𝑙, 𝑟之间合法的方案数。𝑓[𝑙][𝑟]表示,点的编号在𝑙, 𝑟之间合法的方案数。当一个栅栏两侧没有相同颜色的企鹅时定义这
个栅栏是有效的。
然后就可以直接区间DP了。考虑计算ℎ[𝑙][𝑟][𝑛𝑜𝑤],枚举中间点𝑛𝑙,当< 𝑛𝑙, 𝑙 >合法时进行转移,分类讨论𝑛𝑜𝑤和三角形(𝑙, 𝑛𝑙, 𝑟)中有没有点的情况,再考虑向量< 𝑙, 𝑛𝑙 >右侧有没有企鹅,没有直接转移,有的话,考虑多边形(𝑙, 𝑙 + 1, 𝑛𝑙 − 1, 𝑛𝑙)内有没有点分类讨论一下即可。考虑计算𝑔[𝑙][𝑟],枚举中间点𝑛𝑟,当< 𝑛𝑟, 𝑙 >合法时进行转移,如果向量< 𝑟, 𝑙 >左侧和向量< 𝑙, 𝑛𝑙 >左侧的企鹅的并集为空𝑔[𝑙][𝑟] += ℎ[𝑙][𝑛𝑟][1],否则如果向量< 𝑛𝑟 + 1, 𝑟 >的企鹅和上面两个的并集为空𝑔[𝑙][𝑟] += ℎ[𝑙][𝑛𝑟][1] * 𝑓[𝑛𝑟 + 1][𝑟]。𝑓[𝑙][𝑟]可以轻松的从𝑔[𝑙][𝑟]转移过来。
【分析】
题解非常地神,直接给出了一个dp,这里从头分析为什么要这么做。
首先很容易确定一条边是否可以选(如果这条边两侧有相同颜色的企鹅,那么不能选)
那么现在问题变成了知道一些边能选不能选,要求选择一些边使得它们围成多个多边形,且每个企鹅都在一个多边形里,每个多边形里都有至少一个企鹅。
问题是在一个环上选出一些边,怎么就变成区间dp了呢?
考虑最朴素的暴力:枚举每条边选不选,再检查是否满足条件。所以第一步,我们考虑枚举一条边,选择任意一个点作为起始点,如下图选择了这个绿色的点
为了不重复,我们枚举这个点沿逆时针方向遇到的第一个有边的点。具体地说,这个点可能有边,可能没有边,如果没有边,我们按逆时针顺序继续枚举下一个点,直到枚举到一个有边的点,标为蓝色。
接下来继续枚举第二的点作为边的另一端,同样的,为了不重复,这个点从$r$开始按顺时针顺序枚举,找到第一个与$i$有边的$j$,由于$i$是从$l$开始第一个有边的点,所以$[l,i+1]$里面的点都不会再有边了,而$[j+1,r]$里点只有它们内部才可能有边,所以$<i,j>$这条边只能与$[i+1,j-1]$里面的点构成多边形了。而这恰好是一个区间,所以可以考虑区间dp。我们现在关心的是什么?是$[i,j]$这段区间里,能形成多少种如红线的方案。具体来说就是选了某些边,且$i,j$都已经作为一个端点,这样就可以在添加了$<i,j>$之后合并了,记方案数为$h(i,j)$(暂时不考虑企鹅的限制),那么此时答案需要加上$h(i,j)*([j+1,r]里连边的方案数)$。
再来看$[j+1,r]$里连边的方案数怎么求,我们发现这又是可以看成一个环,我们又回到了一个和原来的问题一样的问题,可以用同样的方式求解,不妨用$f(l,r)$表示,这样答案就是$f(0,n-1)$。
现在我们考虑$h(l,r)$怎么求,同样的,我们可以枚举$l$出去的第一条边$<l,i>$。
那么现在有两种情况:
1.$<i,r>$之间有边,这样的方案数就是$f(l+1,i-1)f(i+1,r-1)$。
2.$<i,r>$之间没有边,这样的方案数是$h(i,r)f(l+1,i-1)$。
至此,在忽略[每个企鹅都在一个多边形里,每个多边形里都有至少一个企鹅]这个限制的情况下,我们已经可以在$O(n^4)$解决这个问题了。
我们发现求解h的复杂度是$O(n^3)$的,而求解f是$O(n^4)$的,所以考虑优化f的转移。
我们发现$$f(l,r) = \sum_{l \leqslant i \leqslant r} \sum_{i < j \leqslant r} h(i,j)f(j+1,r)$$
而$$f(l-1,r) = \sum_{l-1 \leqslant i \leqslant r} \sum_{i < j \leqslant r} h(i,j)f(j+1,r)$$
对于$l \leqslant i \leqslant r$的部分,两个状态是一样的,所以我们自然不必每次都枚举。
一种思路是记$$e(l,r) = \sum_{l < j \leqslant r} h(l,j)f(j+1,r)$$
则$$f(l,r) = \sum_{l \leqslant i \leqslant r} e(i,j)$$
$e$可以理解为$l$一定选的情况下$[l,r]$内的方案数。
这样$f,h,e$可以在$O(n)$的时间内转移,总时间复杂度$O(n^3)$.
现在是时候考虑[每个企鹅都在一个多边形里,每个多边形里都有至少一个企鹅]这个限制了。
实际上也很简单,我们只要保证在闭合一个多边形的时候它里面有企鹅,并且在转移的时候保证空出来的图形里没企鹅即可。
这需要在$h$的状态里记录这个未封闭的图形里是否有企鹅,在转移$f$的时候保证S1没有企鹅,在转移h时保证S2没企鹅,在通过连接$<i,r>$转移$h$的时候保证S3没有企鹅。
我为什么用了$f,h,e$却不用$g$呢?因为在代码里$g(l,r)$表示连了$<l,r>$这条边$[l,r]$内的方案数,实际上就是$h(l,r,1)$。
现在来回答开头的问题
在环上选点是如何变成了区间dp?
【notice】 mod 100007是会爆int的,一开始注意到了写程序的时候却先忽略了,后来查了半天还以为转移写错了。。
【代码】
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 230, mod = 100007;
int cnt[N][N], ok[N][N], col[N]; // cnt[l][r] <l, r> 右侧的企鹅数量
int n, m, r;
int get_num(int a[], int n) {
int res = m;
a[n] = a[0];
for(int i = 0; i < n; i++) {
res -= cnt[a[i]][a[i+1]];
}
//assert(res >= 0);
return res;
}
void addit(int &x, int y) {
if((x += y) >= mod) x -= mod;
}
int f[N][N], h[N][N][2], e[N][N];
int E(int, int);
int F(int l, int r) {
if(l >= r || !cnt[l][r]) return 1;
int &res = f[l][r];
if(res >= 0) return res;
res = 0;
for(int i = l; i+1 < r; i++) {
if(cnt[i][r] + cnt[r][l] == m) addit(res, E(i, r));
}
return res;
}
int H(int l, int r, int s) {
if(r - l + 1 <= 2) return 0;
int &res = h[l][r][s];
if(res >= 0) return res;
res = 0;
for(int i = l + 1; i < r; i++) if(ok[l][i]) {
int a[] = {l, l + 1, i - 1, i, 0}, b[] = {l, i, r, 0};
if(i - l + 1 >= 3 && get_num(a, 4)) continue; // 空出的四边形内有企鹅
int sc = get_num(b, 3) > 0;/* 当前三角形里是否有点 */
// 不连<i, r>从h转移
if(s == sc) {
addit(res, (LL) F(l + 1, i - 1) * H(i, r, 0) % mod);
}
if(s) {
addit(res, (LL) F(l + 1, i - 1) * H(i, r, 1) % mod);
}
// 连接<i, r>直接转移
if(ok[i][r] && s == sc) {
int c[] = {i, i + 1, r - 1, r, 0};
if(r - i + 1 >= 3 && get_num(c, 4));
else addit(res, (LL) F(l + 1, i - 1) * F(i + 1, r - 1) % mod);
}
}
return res;
}
#define G(l, r) (ok[l][r] ? H(l, r, 1) : 0)
int E(int l, int r) {
int &res = e[l][r];
if(res >= 0) return res;
res = 0;
for(int i = l + 1; i <= r; i++) if(ok[l][i]) {
int a[] = {l, i, i + 1, r, 0};
if(i < r && get_num(a, 4));
else addit(res, (LL) G(l, i) * F(i + 1, r) % mod);
}
return res;
}
struct Point {
double x, y;
Point() {}
Point(double x, double y) : x(x), y(y) {}
Point operator - (const Point &rhs) const {
return Point(x - rhs.x, y - rhs.y);
}
} p[N], q[N];
double Cross(const Point &a, const Point &b) {
return a.x * b.y - a.y * b.x;
}
int idx(char c) {
if('a' <= c && c <= 'z') return c - 'a';
return c - 'A' + 26;
}
void input() {
scanf("%d%d%d", &n, &m, &r);
double pi = acos(-1.0);
for(int i = 0; i < n; i++) {
double ang = 2 * pi * i / n;
p[i] = Point(r * cos(ang), r * sin(ang));
}
char str[9];
for(int i = 0; i < m; i++) {
scanf("%s%lf%lf", str, &q[i].x, &q[i].y);
col[i] = idx(str[0]);
}
}
bool OnLeft(const Point &p, const Point &a, const Point &b) {
assert(fabs(Cross(b - a, p - a)) > 1e-3);
return Cross(b - a, p - a) > 0;
}
void init() {
LL st[N][N] = {0};
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) if(i != j) {
int &res = cnt[i][j];
for(int k = 0; k < m; k++) {
if(!OnLeft(q[k], p[i], p[j])) {
res++;
st[i][j] |= 1LL << col[k];
}
}
}
}
for(int i = 0; i < n; i++) {
for(int j = i+1; j < n; j++) {
if(st[i][j] & st[j][i]);
else ok[i][j] = ok[j][i] = 1;
}
}
}
//#define dout(x) cerr << #x << " = " << x << endl
int main() {
freopen("penguin.in", "r", stdin);
freopen("penguin.out", "w", stdout);
input(), init();
memset(f, -1, sizeof f);
memset(h, -1, sizeof h);
memset(e, -1, sizeof e);
printf("%d\n", F(0, n-1));
return 0;
}