「考试总结」ZROI-21-CSP7连-DAY5 总结

#T1 游戏

#题意简述

给定 \(n(n\leq10^{100000})\),输出将 \(n\) 的数位重排列可以得到的最大的 \(30\) 的倍数。

#大体思路

直接判有没有 \(0\)、可不可以被 \(3\) 整除,若可以,直接排序即可。时间复杂度 \(O(n\log n)\).

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int N = 100010;

char s[N]; int n, cnt, sum;

inline bool cmp(char a, char b) {return a > b;}

int main() {
scanf("%s", s); n = strlen(s);
for (int i = 0; i < n; ++ i)
sum += s[i] - '0', cnt += (s[i] == '0');
if (!cnt || sum % 3) printf("-1");
else sort(s, s + n, cmp), printf("%s", s);
return 0;
}

#T2 排列

#题意简述

给定一个 \(1\dots n(n\leq10^4)\) 的排列 \(P\),问对于 \(x\in[1,n]\)\(P\) 是否是 \(x\)-排列,用长度为 \(n\) 的 01 串表示答案。

定义一个排列 \(P\)\(m\)-排列当且仅当存在一个长度为 \(m\) 的子段 \(Q\)\(\forall x\in[1,m]\),有 \(x\in Q\).

#大体思路

考虑 \(P\) 一定是 \(1\)-排列,注意到如果 \(P\)\(x\)-排列,对应的子段为 \(Q_1\),那么如果 \(P\)\((x+1)\)-排列,对应子段为 \(Q_2\),那么一定有 \(Q_1\subset Q_2\),所以可以考虑枚举 \(x\),用双指针维护 \(Q\),每次考虑当前 \(|Q|\) 是否等于 \(x\) 得到答案。时间复杂度 \(O(n)\)

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const int N = 10010;

int n, a[N], ans[N], pos;

int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++ i) {
scanf("%d", &a[i]);
if (a[i] == 1) pos = i;
}
int l = pos, r = pos;
for (int i = 1; i <= n; ++ i) {
while (l > 1 && a[l - 1] <= i) -- l;
while (r < n && a[r + 1] <= i) ++ r;
if (r - l + 1 >= i) ans[i] = 1;
else ans[i] = 0;
}
for (int i = 1; i <= n; ++ i)
printf("%d", ans[i]);
return 0;
}

#T3 照明

#题意简述

给定一张 \(n(n\leq5\times10^4)\) 个点 \(m(m\leq2\times10^5)\) 条边的有向无环图,可以将每条边任意染成 RGB 三种颜色。要求不允许出现一条长度大于等于 \(42\) 的路径颜色相同,给出一种染色方案。

#大体思路

一道挺有意思的构造题。注意到 \(42^3>5\times10^4\),于是我们将所有点按照拓扑序排序,将连续的 \(42\) 个点分为一小组,\(42\) 个连续的小组分为一大组,每一小组中任意两点之间的边染为 R,在同一大组中但不在同一小组的点之间的边染 G,将大组与大组之间的边染 B,由于每一小组中的最长路径小于等于 \(41\),每一大组中的最长相同颜色路径长度小于等于 \(41\),大组的数量一定小于 \(42\),所以构造得到的边的颜色一定满足题意。

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const int N = 200010;
const int INF = 0x3fffffff;

struct Edge {int u, v, nxt;} e[N], ae[N];

int ecnt(1), icnt[N], head[N];

inline void add_edge(int u, int v) {
e[ecnt].u = u, e[ecnt].v = v, ++ icnt[v];
e[ecnt].nxt = head[u], head[u] = ecnt ++;
}

int n, m, q[N], frt, tal, T1, T2, T3, type[N][3];

/*Use topo to divide the node to different groups.*/
void topo() {
frt = 0, tal = -1;
for (int i = 1; i <= n; ++ i)
if (!icnt[i]) q[++ tal] = i;
while (frt <= tal) {
int now = q[frt ++]; (++ T1) %= 42;
if (T1 % 42 == 1) ++ T2; if (T2 % 42 == 1) ++ T3;
type[now][2] = T3, type[now][1] = T2;
for (int i = head[now]; i; i = e[i].nxt)
if (!(-- icnt[e[i].v])) q[++ tal] = e[i].v;
}
}

int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++ i) {
scanf("%d%d", &ae[i].u, &ae[i].v);
add_edge(ae[i].u, ae[i].v);
}
topo();
for (int i = 1; i <= m; ++ i) {
int u = ae[i].u, v = ae[i].v;
if (type[u][1] == type[v][1]) printf("R\n");
else if (type[u][2] == type[v][2]) printf("G\n");
else printf("B\n");
}
return 0;
}

#T4 相交

期望得分 \(30pts\)\(O(n^4)\) 做法可拿到 \(95pts\)(你永远可以相信蟹老板的数据.jpg

#题意简述

给定平面直角坐标系中 \(n(n\leq10^3)\) 个点的坐标 \((x_i,y_i)(-10^9\leq x_i,y_i\leq10^9)\),保证不存在三点共线,问有多少条线段不与其他任意一条线段相交(端点相同不算相交)。

#大体思路

首先,发现合法的边即是在所有点形成的封闭图形(可能是凹多边形)的所有三角剖分中都出现的边,于是直接通过加边维护,随意求出一组三角剖分的时间复杂度为 \(O(n^3)\),再依次判断每条边是否可行的时间复杂度为 \(O(n^3)\),不可接受。

先考虑优化求得三角剖分的过程。首先我们可以先得到整张图的凸包,通过向对角线连边得到最初的三角形集合,再用不在凸包边界上的点将已有的三角形进行分割得到完整的三角剖分。维护凸包的时间复杂度是 \(O(n^2)\) 的(当然也可以做到 \(O(n\log n)\),初始三角形的个数是 \(O(n)\) 级别的,于是分割三角形的时间复杂度也可以做到 \(O(n^2)\),总体时间复杂度是 \(O(n^2)\)

然后来优化判定相交的部分。考虑一个线段 \(PQ\),不失一般性地假设直线 \(PQ\) 是垂直的,且 \(P\)\(Q\) 之上,将所有其他点分为 \(A,B\) 两个集合,其中对于 \(A_i\in A\),都在 \(PQ\) 左部,按 \(\angle A_iPQ\) 降序排序,对于 \(B_i\in B\),都在 \(PQ\) 右部,按 \(\angle PQB_i\) 降序排序。

对于某个点 \(A_i\),令 \(f(i)\) 表示最大的 \(j\) 满足 \(\angle A_iPB_j<\pi\)(如果存在),显然这样的 \(f(i)\) 是非严格递增的,可以通过双指针得到。令 \(g(j)\) 表示按 \(\angle PQB_j\) 降序排序后 \(B_j\) 的新的编号。

我们发现,如果 \(\exists j'\) 使得 \(PQ\)\(A_iB_{j'}\) 相交,那么 \(A_iB_{j'}\) 一定在直线 \(A_iP\) 之下,\(A_{i}Q\) 之上,于是令 \(j'=\arg\max_{1\leq j\leq f(i)}g(j)\),显然如果 \(A_iB_{j'}\)\(PQ\) 都没有交点,显然不会再有任何一条 \(A_iB_j\)\(PQ\) 相交了。排序、求出 \(g(j)\) 的时间复杂度为 \(O(n\log n)\),所以总体时间复杂度为 \(O(n^2\log n)\).

#亿些细节

这里介绍一个很有用的函数 \(ccw(A,B,C)\),并说明如何用它来解决上面大部分的比较和判断。\(ccw\) 的定义如下: \[ ccw(A,B,C)=x_A\cdot(y_B-y_C)+x_B\cdot(y_C-y_A)+x_C\cdot(y_A-y_B), \] 于是我们不难发现,当 \(A,B,C\) 在平面中是逆时针顺序排列时,\(sign(ccw(A,B,C))=1\),如果为顺时针排列则有 \(sign(ccw(A,B,C))=-1\)\(sign(x)\) 表示取符号),利用这条性质,可以直接解决很多判断和比较。

  1. 判断两线段相交;

不难发现应当有 \(ccw(A,B,C)>0,ccw(A,B,D)<0\) 以及 \(ccw(C,D,A)<0,ccw(C,D,B)>0\),于是可以直接判断。

  1. 维护凸包;

发现凸包边界从最左下角 \(P_0\) 开始顺时针方向应当有任意边界点 \(ccw(P_0,P_x,P_{x+k})<0(k>0)\),如果有一点使得 \(ccw>0\),那么一定不在凸包边界上;

  1. 判断在三角形三边同侧;

如图,当 \(D\)\(\Delta ABC\) 内部时,应当有 \[ \begin{cases} ccw(D,A,C)>0,\\ ccw(D,C,B)>0,\\ ccw(D,B,A)>0, \end{cases} \]

  1. 按照角度大小排序;

这一点在代码的 cmp() 中有体现,利用相对位置即可。

更多细节见代码及注释

#Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#define ll long long
#define pii pair <int, int>

const int N = 1010;

template <typename T> inline void read(T &x) {
x = 0; int f = 1; char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}

template <typename T> inline T Max(T &a, T &b) {return a > b ? a : b;}
template <typename T> inline T Min(T &a, T &b) {return a < b ? a : b;}
template <typename T> inline int Sign(T x) {return x < 0 ? -1 : (x > 0 ? 1 : 0);}

struct Traingle {
int a, b, c;
bool operator < (const Traingle &x) const {
return a == x.a ? (b == x.b ? c < x.c : b < x.b) : a < x.a;
}
};

struct Point {
ll x, y;

Point operator - (const Point &b) const {return (Point){x - b.x, y - b.y};}
Point operator + (const Point &b) const {return (Point){x + b.x, y + b.y};}
inline ll operator * (const Point &b) const {return x * b.x + y * b.y;}
inline ll operator ^ (const Point &b) const {return x * b.y - y * b.x;}
bool operator == (const Point &b) const {return x == b.x && y == b.y;}
bool operator < (const Point &b) const {return x == b.x ? y < b.y : x < b.x;}
} p[N];

int CCW(const Point &a, const Point &b, const Point &c) {
return Sign(a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
}

bool INTERSECT(const Point &a, const Point &b, const Point &c, const Point &d) {
if (a == b || a == c || a == d || b == c || b == d || c == d) return false;
return CCW(a, b, c) != CCW(a, b, d) && CCW(c, d, a) != CCW(c, d, b);
}

inline int ccw(int i, int j, int k) {return CCW(p[i], p[j], p[k]);}

inline bool intersect(int i, int j, int k, int o) {return INTERSECT(p[i], p[j], p[k], p[o]);}

int cmp_std;
inline bool cmp(int a, int b) {return ccw(cmp_std, a, b) == 1;}

int n, on_border[N];

vector <pii > edges;
set <Traingle> traingles;

/*Find the convex hull, and get the initial traingles at the same time.*/
/*Notice: We have no necessary to save the convex hull, it's useless.*/
void traingles_init() {
int lst = 0;
do {
int nxt = 0; if (!lst) nxt = 1;
/*If Starting from the initial point, all points on the boundary
should be arranged in a clockwise direction.*/
for (int i = 0; i < n; ++ i)
if (ccw(lst, nxt, i) == 1) nxt = i;
/*Add the edge and the traingle to the set.*/
edges.push_back({lst, nxt});
if (lst && nxt) traingles.insert({0, nxt, lst});
on_border[lst] = true; lst = nxt;
} while (lst);
}

/*Use the points which are not on the boundary to try to divide the traingles.*/
void traingles_divide() {
for (int i = 0; i < n; ++ i) {
if (on_border[i]) continue;
Traingle target;
/*If the point is on the same side for all three sides of a triangle,
* it must be included by this triangle. We use clockwise and counter-
* clockwise directions to judge whether it is on the same side*/
for (auto it : traingles)
if (ccw(it.a, it.b, i) == 1
&& ccw(it.b, it.c, i) == 1
&& ccw(it.c, it.a, i) == 1) {
target = it; break;
}
/*After find the corresponding traingle, delete it from the set,
* and add the new traingles and edges to the set.*/
traingles.erase(target);
traingles.insert({target.a, target.b, i});
traingles.insert({target.b, target.c, i});
traingles.insert({target.c, target.a, i});
edges.push_back({i, target.a});
edges.push_back({i, target.b});
edges.push_back({i, target.c});
}
}

int get_ans() {
int res = 0;
for (auto &e : edges) {
int u = e.first, v = e.second;
vector <int> a, b; cmp_std = u;
/*Divide the points to two groups.*/
for (int i = 0; i < n; ++ i) {
if (ccw(u, v, i) == -1) a.push_back(i);
if (ccw(u, v, i) == +1) b.push_back(i);
}
sort(a.begin(), a.end(), cmp);
sort(b.begin(), b.end(), cmp);
vector <int> c = b; cmp_std = v;
sort(c.begin(), c.end(), cmp);
vector <int> pos(n);
for (int i = 0; i < (int)c.size(); ++ i) pos[c[i]] = i;
bool ok = true;
int j = 0, k = -1;
/*use two-pointers to get the answer.*/
for (int i = 0; i < (int)a.size(); ++ i) {
while (j < (int)b.size() && ccw(a[i], u, b[j]) == -1)
k = max(k, pos[b[j]]), ++ j;
if (k != -1) ok &= !intersect(u, v, a[i], c[k]);
}
if (ok) ++ res;
}
return res;
}

int main() {
read(n);
for (int i = 0; i < n; ++ i) read(p[i].x), read(p[i].y);
sort(p, p + n); raingles_init(); raingles_divide();
printf("%d", get_ans()); return 0;
}