P7529 [USACO21OPEN] Permutation G题解
[USACO21OPEN] Permutation G
题目描述
Bessie 在二维平面上有 N N N 个最爱的不同的点,其中任意三点均不共线。对于每一个 1 ≤ i ≤ N 1\le i\le N 1≤i≤N,第 i i i 个点可以用两个整数 x i x_i xi 和 y i y_i yi 表示。
Bessie 按如下方式在这些点之间画一些线段:
-
- 她选择这 N N N 个点的某个排列 p 1 , p 2 , … , p N p_1,p_2,\dots,p_N p1,p2,…,pN 。
-
- 她在点 p 1 p_1 p1 和 p 2 p_2 p2 、 p 2 p_2 p2 和 p 3 p_3 p3、 p 3 p_3 p3 和 p 1 p_1 p1 之间画上线段。
-
- 然后依次对于从 4 4 4 到 N N N 的每个整数 i i i,对于所有 j < i j<i j<i,她从 p i p_i pi 到 p j p_j pj 画上一条线段,只要这条线段不与任何已经画上的线段相交(端点位置相交除外)。
Bessie 注意到对于每一个 i i i ,她都画上了恰好三条新的线段。计算 Bessie 在第 1 1 1 步可以选择的满足上述性质的排列的数量,结果对 1 0 9 + 7 10^9+7 109+7 取模。
输入格式
输入的第一行包含 N N N。
以下 N N N 行,每行包含两个空格分隔的整数 x i x_i xi 和 y i y_i yi。
输出格式
输出一行一个整数表示方案数对 1 0 9 + 7 10^9+7 109+7 取模后的结果。
样例 #1
样例输入 #1
4
0 0
0 4
1 1
1 2
样例输出 #1
0
样例 #2
样例输入 #2
4
0 0
0 4
4 0
1 1
样例输出 #2
24
样例 #3
样例输入 #3
5
0 0
0 4
4 0
1 1
1 2
样例输出 #3
96
提示
样例一解释
没有排列满足该性质
样例二解释
所有排列均满足该性质
样例解释三
一个满足该性质的排列为 ( 0 , 0 ) , ( 0 , 4 ) , ( 4 , 0 ) , ( 1 , 2 ) , ( 1 , 1 ) (0,0),(0,4),(4,0),(1,2),(1,1) (0,0),(0,4),(4,0),(1,2),(1,1) 。对于这个排列,
- 首先,她在 ( 0 , 0 ) , ( 0 , 4 ) (0,0),(0,4) (0,0),(0,4) 和 ( 4 , 0 ) (4,0) (4,0) 两两之间画上线段。
- 然后她从 ( 1 , 2 ) (1,2) (1,2) 向 ( 0 , 0 ) (0,0) (0,0) , ( 0 , 4 ) (0,4) (0,4) 和 ( 4 , 0 ) (4,0) (4,0) 画上线段。
- 最后,她从 ( 1 , 1 ) (1,1) (1,1) 向 ( 1 , 2 ) (1,2) (1,2) , ( 4 , 0 ) (4,0) (4,0) 和 ( 0 , 0 ) (0,0) (0,0) 画上线段。
数据范围与约定
3 ≤ N ≤ 40 3\le N \le 40 3≤N≤40, 0 ≤ x i , y i ≤ 1 0 4 0\le x_i,y_i \le 10^4 0≤xi,yi≤104
题解
解释代码里有
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define resetIO(x) { freopen(x".in", "r", stdin); freopen(x".out", "w", stdout); }
template<class T> inline void read(T &x){
T f = 1; x = 0; char ch = getchar();
for( ; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -f;
for( ; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
x *= f;
}
template<class T> inline void write(T x){
if(x < 0) { putchar('-'); x = -x; }
if(x > 9) write(x / 10); putchar((x % 10) ^ 48);
}
const int maxn = 41;
const int inf = 1LL << 60;
const int mod = 1e9+7;
// 向量模版
struct Vec{
int x, y;
Vec(){ }
Vec(int _x, int _y) : x(_x), y(_y) { }
Vec(pair<int, int> p) : x(p.first), y(p.second) { }
// 向量加法
Vec operator+(const Vec &o) const {
return Vec(x + o.x, y + o.y);
}
// 向量减法
Vec operator-(const Vec &o) const {
return Vec(x - o.x, y - o.y);
}
// 向量数乘
Vec operator*(const int k) const {
return Vec(x * k, y * k);
}
// 向量点乘
int operator*(const Vec &o) const {
return x * o.x + y * o.y;
}
// 向量叉乘
int operator^(const Vec &o) const {
return x * o.y - y * o.x;
}
};
struct Line{
Vec a, b;
Line(){ }
Line(Vec _a, Vec _b) : a(_a), b(_b) { }
// 获取线段对应的向量
Vec vector() const {
return b - a;
}
// 判断两个点是否在线段的同侧
bool side(Vec v1, Vec v2) const {
Vec x = vector(), y = v1 - a, z = v2 - a;
return (x ^ y) * (x ^ z) > 0;
}
};
int n, dp[maxn][maxn][maxn][maxn];
pair<int, int> a[maxn];
// 判断点在三角形内
bool inner_triangle(int p1, int p2, int p3, int i){
Vec v1(a[p1]), v2(a[p2]), v3(a[p3]), vi(a[i]);
Line l1(v2, v3), l2(v1, v3), l3(v1, v2);
// 判断点vi和v1在l1的同侧,和v2在l2的同侧以及和v3在l3的同侧
return l1.side(v1, vi) && l2.side(v2, vi) && l3.side(v3, vi);
}
// 判断点在红色区域中,如果是则更新成新的三角形三个端点
bool outer_triangle(int &p1, int &p2, int &p3, int i){
Vec v1(a[p1]), v2(a[p2]), v3(a[p3]), vi(a[i]);
Line l1(v2, v3), l2(v1, v3), l3(v1, v2);
// 判断点vi和v1在l1的异侧并且vi和v2在l2的异侧,如果符合则更新v3,下同
if((!l1.side(v1, vi)) && (!l2.side(v2, vi))) { p3 = i; return true; }
if((!l1.side(v1, vi)) && (!l3.side(v3, vi))) { p2 = i; return true; }
if((!l2.side(v2, vi)) && (!l3.side(v3, vi))) { p1 = i; return true; }
return false;
}
// 记忆化搜索(dp),dp[p1][p2][p3][k]表示
// 以p1,p2,p3为三角形端点且已经放了k个点的方案数
int dfs(int p1, int p2, int p3, int k){
// k=n时返回1
if(k == n) return 1;
// 排序三角形端点
if(p1 > p2) swap(p1, p2);
if(p1 > p3) swap(p1, p3);
if(p2 > p3) swap(p2, p3);
if(dp[p1][p2][p3][k] != -1) return dp[p1][p2][p3][k];
// 用&引用来写记忆化会比较舒服
int &ret = dp[p1][p2][p3][k]; ret = 0;
// 计算三角形内点的个数,最后一起计算(算是个优化)
int cnt_in = 0;
for(int i=1; i<=n; i++){
// 新的点不能和三角形端点重复
if(p1 == i || p2 == i || p3 == i) continue;
// 分情况讨论
if(inner_triangle(p1, p2, p3, i)){
// 计算三角形内点的个数
cnt_in ++;
}
else{
// 为了防止原来的p1,p2,p3被更改,这里新开了3个变量
int tp1 = p1, tp2 = p2, tp3 = p3;
// 判断是否在红色区域
if(outer_triangle(tp1, tp2, tp3, i))
(ret += dfs(tp1, tp2, tp3, k + 1)) %= mod;
}
}
// 判断已经放置的个数k是否超过三角形内的点个数+3个端点,如果超过,就不能再在三角形内放置了
if(cnt_in + 3 > k)
(ret += (dfs(p1, p2, p3, k + 1) * (cnt_in + 3 - k))) %= mod;
return ret;
}
signed main(){
// 读入+初始化
read(n);
for(int i=1; i<=n; i++) read(a[i].first), read(a[i].second);
memset(dp, -1, sizeof(dp));
int ans = 0;
// 枚举前三个点的顺序
for(int p1=1; p1<=n; p1++){
for(int p2=p1+1; p2<=n; p2++){
for(int p3=p2+1; p3<=n; p3++){
(ans += dfs(p1, p2, p3, 3)) %= mod;
}
}
}
// 因为前三个点可以任意交换位置,所以答案要乘上6
write((ans * 6) % mod); putchar('\n');
return 0;
}