0%

题目大意

思路讲解

image

原先的凸性检测没法检测出这样子的五角星。

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
// 不允许自交的多边形,顺序都可以,会把 poly 变为 CCW 顺序(逆时针)
// AC https://qoj.ac/submission/2002121
bool isConvex(vector<Point> &poly) {
ll n = poly.size();
Point p1 = poly[1] - poly[0], p2 = poly[2] - poly[0];
if (sgn(cross(p1, p2)) <= 0) {
reverse(poly.begin(), poly.end());
}
// 找到字典序最小的点
ll pos_mn = 0;
for (int i = 1; i < n; ++i) {
if (poly[i] < poly[pos_mn]) {
pos_mn = i;
}
}
// Performs a left rotation on a range of elements
// 向左循环移动,把 pos_mn 移动到第一个 [0]
rotate(poly.begin(), poly.begin() + pos_mn, poly.end());
// 在一个标准的凸多边形中,如果我们站在最左下角的点(poly[0])往外看
// 其他所有顶点 poly[1], poly[2], ..., poly[n-1] 应该按照逆时针方向整齐排列。
// 从而排除星形(自交)多边形和乱序点集。
for (int i = 1; i < n - 1; ++i) {
if (sgn(cross(poly[i] - poly[0], poly[i + 1] - poly[0])) <= 0) {
return false;
}
}
// 判断单个内角是否 ≥ 180。
for (int i = 0; i < n; ++i) {
ll i1 = (i + 1) % n, i2 = (i + 2) % n;
Vector a = poly[i1] - poly[i], b = poly[i2] - poly[i1];
// 加上大于等于,就相当于排除所有的退化情况,不允许内角为 180 度
if (sgn(cross(a, b)) <= 0) {
return false;
}
}
return true;
}

AC代码

AC
https://qoj.ac/submission/2002045

AC https://qoj.ac/submission/2002100

AC https://qoj.ac/submission/2002121

心路历程(WA,TLE,MLE……)

题目大意

思路讲解

AC代码

AC
https://qoj.ac/submission/2001485

心路历程(WA,TLE,MLE……)

题目大意

在一个基于2D物理的视频游戏中,两个角色PPQQ在战场上移动。每个角色都由一个凸多边形表示,用作其碰撞箱。当两个碰撞箱重叠时,游戏会计算它们的交集区域作为碰撞伤害。

然而,由于竞技场中不可预测的风流,在任意平移向量t=(tx,ty)\mathbf{t} = (t_x, t_y)的作用下,角色QQ可以被位移,但不能旋转或翻转。您需要计算当QQ均匀随机放置以确实与PP发生碰撞时的预期碰撞伤害,即它们的交集具有正面积。

更正式地,定义f(t)f(\mathbf{t})为表示角色PP的多边形和表示角色QQ的多边形在沿t\mathbf{t}平移后的交集区域的面积。DR2D \subseteq \mathbb{R}^2为所有平移向量t\mathbf{t}的集合,使得f(t)>0f(\mathbf{t}) > 0。可以证明DD具有正面积,记为D|D|。您需要计算1DDf(t)dt\frac{1} {|D|} \iint\limits_{D} f(\mathbf{t}) \, d\mathbf{t}

思路讲解

1. 分子分析 (Df(t)dt\iint_D f(\mathbf{t}) \, d\mathbf{t})

根据积分几何中的运动学公式,两个形状在所有可能的平移下的交集面积的积分,等于这两个形状面积的乘积。

R2Area(PQ(t))dt=Area(P)×Area(Q)\iint_{\mathbb{R}^2} \text{Area}(P \cap Q(\mathbf{t})) \, d\mathbf{t} = \text{Area}(P) \times \text{Area}(Q)

因此,分子只需计算两个多边形面积的乘积。

原因可以用以下离散化像素法解释:

想象一下,我们将 PPQQ 都放在一个非常细的网格纸上,把它们看作是由无数个微小的像素点组成的。

  • 假设 PPNN 个像素点组成(代表面积 SPS_P)。

  • 假设 QQMM 个像素点组成(代表面积 SQS_Q)。

我们要求的积分 Area(PQ(t))dt\iint \text{Area}(P \cap Q(\mathbf{t})) \, d\mathbf{t},本质上是在问:当我们把 QQ 移动到所有可能的位置时,总共有多少次“像素重叠”发生?

让我们换个角度思考:

  1. QQ 中的任意一个特定像素点 q0q_0

  2. QQ 在平面上到处移动时,这个点 q0q_0 会经过平面的每一个位置。

  3. 它什么时候会和 PP 发生“重叠”?也就是当 q0q_0 落在 PP 内部的时候。

  4. 因为 PPNN 个像素,所以 q0q_0 会在 NN 个不同的位置(平移向量)上为“总重叠面积”贡献 1 个单位。

  5. 现在,对 QQ 中的每一个像素点都重复这个过程。QQ 总共有 MM 个像素点。

  6. 因此,总的重叠贡献 = (QQ 的像素数) ×\times (PP 的像素数) = M×NM \times N

这就对应了 Area(Q)×Area(P)\text{Area}(Q) \times \text{Area}(P)

总的来说,就是以 QQ 为主,那么 QQ 中每一个点都可以和这个 PP 中每一个点相交,PP 中点的数量就是这个 Area(P)\text{Area}(P),那么 QQ 中一共有 Area(Q)\text{Area}(Q) 个点,总的积分就是 Area(P)×Area(Q)\text{Area}(P)\times\text{Area}(Q)

2. 分母分析 (D|D|)

集合 DD 表示所有能让 PPQQ 接触或重叠的平移向量 t\mathbf{t}。这个集合实际上就是 PPQ-QQQ 关于原点的中心对称图形)的闵可夫斯基和

D=P(Q)D = P \oplus (-Q)

由于 PPQQ 都是凸多边形,它们的闵可夫斯基和 DD 也是一个凸多边形。

构建 DD 的方法

凸多边形的闵可夫斯基和可以通过将两个多边形的所有边向量收集起来,按照极角排序,然后首尾相连构成一个新的多边形。

  • PP 的边向量:Pi+1PiP_{i+1} - P_i

  • Q-Q 的边向量:Q-Q 的边相当于 QQ 的边的反向。即如果 QQ 的边是 Qj+1QjQ_{j+1} - Q_j,那么对应的 Q-Q 的边向量就是 QjQj+1Q_j - Q_{j+1}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Solve() {
ll N, M;
cin >> N >> M;
vector<Point> poly1(N), poly2(M);
for (int i = 0; i < N; ++i) {
cin >> poly1[i];
}
for (int i = 0; i < M; ++i) {
cin >> poly2[i];
poly2[i]=-poly2[i];
}
auto poly_res = minkowski(poly1, poly2);
DB ans = polygonArea(poly1) * polygonArea(poly2) / polygonArea(poly_res);
cout << fsp(12) << ans << "\n";
}

AC代码

AC
https://codeforces.com/gym/106252/submission/361314647

心路历程(WA,TLE,MLE……)

题目大意

"
Sulfox the fennec fox已经建立了一个迷宫,并邀请他的两个朋友Alice和Bob来尝试。迷宫可以抽象为一个简单的、连通的无向图,其中有nn个顶点和mm条边,Alice和Bob在两个指定的不同顶点开始。

在任何玩家移动之前,Sulfox必须为每条无向边选择一个方向,将迷宫变成一个有向图。然后,为了找到对方,两位玩家在定向迷宫内进行回合移动。在每一轮中,他们同时独立行动,不留下任何标记或沟通;相反,他们采取“最愚蠢”的策略:

  • 如果玩家当前位于一个具有一个或多个出边的顶点,则他们会任意穿越一个出边到相邻顶点;

  • 否则,如果他们当前的顶点没有出边,则他们将停留在该顶点。

作为对自己迷宫设计技能感到自豪的Sulfox,希望将通道定向,以便Alice和Bob永远不会相遇。鉴于无向图和Alice和Bob的起始顶点,请帮助Sulfox定向每条边,使得无论Alice和Bob如何在每一轮中做出选择,他们永远不会在任何一轮结束时占据同一顶点。如果不存在这样的定向,请报告。"

思路讲解

我记得是这样子的,只要两个玩家所在顶点不直接相连,就简单了,所有连着两个点的边全部指向这两点,可以把这两个玩家封死。

两个玩家所在顶点直接相连,就找最小的环,所有其他点(边)指向这个环,避免两个玩家离开这个环。

我们需要两次 bfs 实现:

这一部分 bfs 实现,负责这个找到包含 XYX → Y 的环(这个相对容易),如果知道到了,直接输出答案即可。

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
for (int i=1;i<=1;++i) {
g[X].erase(find(all(g[X]),Y));
g[Y].erase(find(all(g[Y]),X));
queue<ll> q;
vector<char> vis(N + 2);
vis[X] = true;
q.push(X);
vector<ll> parent(N + 2);
while (!q.empty()) {
auto u=q.front();
q.pop();
if (vis[Y]) {
break;
}
for (auto to:g[u]) {
if (vis[to]) continue;
vis[to]=true;
parent[to]=u;
q.push(to);
}
}
if (!vis[Y]) {
break;
}
cout<<"Yes\n";
vector<ll> cycle_ls;
for (ll node=Y;node!=X;node=parent[node]) {
edge_ls.erase(lower_bound(all(edge_ls),
pair{min(node,parent[node]),max(node,parent[node])}));
cycle_ls.push_back(node);
cout<<parent[node]<<" "<<node<<"\n";
}
{
edge_ls.erase(lower_bound(all(edge_ls),
pair{X,Y}));
cycle_ls.push_back(X);
cout<<Y<<" "<<X<<"\n";
}
sort(all(cycle_ls));
for (auto [u,v]:edge_ls) {
if (binary_search(all(cycle_ls),u) ) {
cout<<v<<" "<<u<<"\n";
}else if (binary_search(all(cycle_ls),v)){
cout<<u<<" "<<v<<"\n";
}else {
cout<<u<<" "<<v<<"\n";
}
}
return;
}

如果没有包含 XYX\to Y 的这个环,就从 XX 出发,随便找一个这个无弦环即可。

image

为什么需要这两个部分呢?

image

只要我们在第一阶段排除这样子的包含 XYX\to Y 的这个环,我们找到的环上的点的边就不会和 YY 相连。(可以避免出现一些奇奇怪怪的情况)

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
if (isout==false){
g[X].push_back(Y);
g[Y].push_back(X);
// 没有边权,可以直接使用bfs得到最小环。
queue<ll> q;
vector<char> vis(N + 2);
vis[X] = true;
q.push(X);
vector<ll> parent(N + 2);
// 我们找的环,不仅可以包含这个 X - Y 这条边,也可以不包含 X-Y 这条边
// 只要把 X,Y 诱导的我们的环上即可
ll st_ed_cycle = 0, another_pa_st_ed = 0;
while (!q.empty()) {
auto u = q.front();
q.pop();
if (st_ed_cycle != 0) {
break;
}
for (auto to: g[u]) {
// 排除 parent 点
if (to == parent[u]) continue;
// 一旦只要不是 parent 点就遍历到了,那就是遇到环了
if (vis[to]) {
another_pa_st_ed = u;
st_ed_cycle = to;
continue;
}
vis[to] = true;
parent[to] = u;
q.push(to);
}
}
// 一个环都没有找到?说明图本身是一颗树
if (st_ed_cycle == 0) {
cout << "No\n";
return;
}
vector<ll> order_of_cycle,another_part_of_cycle;
vector<char> vis_route(N+2);
// bfs 还原路径,比较难的地方就是环被砍成了了两半
// 第一段路径,使用这个 parent 还原
for (ll i=st_ed_cycle;i!=X;i=parent[i]) {
order_of_cycle.push_back(i);
vis_route[i]=true;
}
vis_route[X]=true;
order_of_cycle.push_back(X);
ll overlap_node=0;
// 第二段路径还原
for (ll i=another_pa_st_ed;overlap_node=i,!vis_route[i];i=parent[i]) {
vis_route[i]=true;
another_part_of_cycle.push_back(i);
}
reverse(all(order_of_cycle));
order_of_cycle.insert(order_of_cycle.end(),all(another_part_of_cycle));
order_of_cycle.push_back(overlap_node);
// order_of_cycle.insert(find(all(order_of_cycle),overlap_node)+1 , order_of_cycle.front());
// 接下来生成这个序列
vector<pair<ll,ll>> out_ls;
for (int i=0;i<SZ(order_of_cycle)-1;++i) {
ll u=order_of_cycle[i],v=order_of_cycle[i+1];
edge_ls.erase(lower_bound(all(edge_ls),
pair{min(u, v), max(u, v)}));
// cout<<u<<" "<<v<<"\n";
out_ls.emplace_back(u,v);
}
if (!vis_route[Y] ) {
edge_ls.erase(lower_bound(all(edge_ls),
pair{X, Y }));
vis_route[Y]=true;
// cout<<Y<<" "<<X<<"\n";
out_ls.emplace_back(Y,X);
}
// for (ll u=overlap_node,v=order_of_cycle.front();false;) {
// edge_ls.erase(lower_bound(all(edge_ls),
// pair{min(u, v), max(u, v)}));
// // cout<<u<<" "<<v<<"\n";
// out_ls.emplace_back(u,v);
// }
for (auto [u,v]:edge_ls) {
if (vis_route[u] ) {
// cout<<v<<" "<<u<<"\n";
out_ls.emplace_back(v,u);
}else if (vis_route[v]){
// cout<<u<<" "<<v<<"\n";
out_ls.emplace_back(u,v);
}else {
// cout<<u<<" "<<v<<"\n";
out_ls.emplace_back(u,v);
}
}
cout<<"Yes\n";
for (auto [u,v]:out_ls) {
cout<<u<<" "<<v<<"\n";
}
}

AC代码

AC
https://qoj.ac/submission/2005037

心路历程(WA,TLE,MLE……)

这道题目的 SPJ 怎么写?

编写这道题 SPJ(Special Judge)的核心难点在于:如何验证“无论怎么走都遇不到”这一条件

因为题目中描述的移动策略是“任意选择一条出边(arbitrarily traverse)”,这意味着玩家的移动是不确定的。你不能只模拟一条路径,而是必须模拟所有可能的路径

核心思路:集合模拟(Set Simulation)

要在 SPJ 中解决这个问题,关键点不是记录 Alice 和 Bob “在哪一个点”,而是记录他们当前可能在哪些点的集合

由于 N300N \le 300,我们可以使用 C++ 的 std::bitset<305> 来高效地维护这个集合。


SPJ 的具体实现步骤

1. 基础校验 (Basic Validation)

首先,必须确保选手输出的图是合法的:

  • 边数对应:选手输出的有向边必须和输入的无向边一一对应(通过 multiset 或排序后比对)。

  • 点范围:点的编号在 1N1 \dots N 之间。

2. 状态定义 (The Critical Point)

这是 SPJ 最关键的部分。

  • SA(t)S_A^{(t)} 为第 tt 轮结束时,Alice 可能所在的所有顶点的集合。

  • SB(t)S_B^{(t)} 为第 tt 轮结束时,Bob 可能所在的所有顶点的集合。

初始状态

SA(0)={Alicestart},SB(0)={Bobstart}S_A^{(0)} = \{Alice_{start}\}, \quad S_B^{(0)} = \{Bob_{start}\}

3. 状态转移 (Transition)

每一轮模拟时,我们需要计算下一轮的集合 S(t+1)S^{(t+1)}。规则如下:

对于集合中的每一个点 uS(t)u \in S^{(t)}

  • 情况 A(有出边):如果 uu 有出边指向 v1,v2,v_1, v_2, \dots,那么在下一轮,Alice/Bob 可能会出现在这些邻居中的任何一个。将 {v1,v2,}\{v_1, v_2, \dots\} 全部加入 S(t+1)S^{(t+1)}

  • 情况 B(无出边):如果 uu 没有出边,根据题目规则,玩家停留在原地。将 uu 加入 S(t+1)S^{(t+1)}

用位运算表示非常快:

1
2
3
4
5
6
7
8
9
10
next_mask = 0;
for (int u = 1; u <= n; u++) {
if (current_mask.test(u)) { // 如果当前可能在 u
if (adj[u].none()) { // u 没有出边
next_mask.set(u); // 下一轮还在 u
} else { // u 有出边
next_mask |= adj[u]; // 下一轮可能在 u 的所有邻居
}
}
}

4. 碰撞检测 (Collision Check)

在任何时刻 tt,如果 Alice 和 Bob 的可能位置集合有交集,说明存在一种走法让他们相遇,判为 WA

  • 判定条件:(S_A & S_B).any() == true

5. 终止条件 (Termination)

模拟不能无限进行下去。我们有两个终止条件:

  1. 相遇:检测到交集,直接 quitf(_wa, ...)****。

  2. **死循环(安全):**如果当前的“状态对” (SA,SB)(S_A, S_B) 曾经出现过,说明我们进入了一个循环,而且在这个循环中没有发生碰撞。那么继续跑下去也不会碰撞。此时判为 AC。

  • 使用 set<pair<string, string>> history 记录 bitset 的状态。

完整的 SPJ 代码结构 (基于 testlib.h)

这是一个标准的写法,你可以直接参考:

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
#include "testlib.h"
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 305;

int main(int argc, char* argv[]) {
registerTestlibCmd(argc, argv);

int t = inf.readInt(); // 读取测试用例数
while (t--) {
int n = inf.readInt();
int m = inf.readInt();
int startA = inf.readInt();
int startB = inf.readInt();

// 1. 读取标准输入的边 (用于校验)
multiset<pair<int, int>> edges;
for (int i = 0; i < m; i++) {
int u = inf.readInt();
int v = inf.readInt();
if (u > v) swap(u, v);
edges.insert({u, v});
}

string ja = ans.readToken(); // Jury's answer
string pa = ouf.readToken(); // Participant's answer

if (ja != pa) {
quitf(_wa, "Answer mismatch! Expected %s, found %s", ja.c_str(), pa.c_str());
}

if (pa == "No") continue; // 如果是 No,且 Jury 也是 No,这组过了

// 2. 建图 & 校验选手输出的边
vector<bitset<MAXN>> adj(n + 1);
vector<bool> has_outgoing(n + 1, false);

for (int i = 0; i < m; i++) {
int u = ouf.readInt();
int v = ouf.readInt();

// 校验边是否存在于原图中
int u_chk = u, v_chk = v;
if (u_chk > v_chk) swap(u_chk, v_chk);
auto it = edges.find({u_chk, v_chk});
if (it == edges.end()) {
quitf(_wa, "Edge %d->%d invalid or used twice", u, v);
}
edges.erase(it);

// 建图
adj[u].set(v);
has_outgoing[u] = true;
}

// 3. 核心模拟逻辑
bitset<MAXN> curA, curB;
curA.set(startA);
curB.set(startB);

// 状态记忆化,防止死循环超时
set<pair<string, string>> visited_states;

bool ok = true;
int steps = 0;

// 这里的 20000 是一个保险值,实际上如果有环,visited_states 会更早截断
while (steps++ < 20000) {
// Check Collision
if ((curA & curB).any()) {
quitf(_wa, "Alice and Bob meet at step %d", steps);
}

// Check Cycle (Memoization)
string sa = curA.to_string();
string sb = curB.to_string();
if (visited_states.count({sa, sb})) {
// 状态重复,且之前没遇到 WA,说明进入安全循环
break;
}
visited_states.insert({sa, sb});

// Move Alice
bitset<MAXN> nextA;
for (int i = 1; i <= n; i++) {
if (curA[i]) {
if (!has_outgoing[i]) nextA.set(i); // Stay
else nextA |= adj[i]; // Move
}
}

// Move Bob
bitset<MAXN> nextB;
for (int i = 1; i <= n; i++) {
if (curB[i]) {
if (!has_outgoing[i]) nextB.set(i); // Stay
else nextB |= adj[i]; // Move
}
}

curA = nextA;
curB = nextB;
}
}

quitf(_ok, "All test cases passed!");
}

总结

这道题 SPJ 的精髓在于不要去想“他们走了哪条路”,而是利用 NN 较小的特点,并行追踪“他们可能在的所有位置”。只要这两个位置集合永远不相交,选手的答案就是正确的。

题目大意

亚历克斯正在规划在一个简化的台湾高速公路系统模型上设立休息区。该系统包含nn个互通立交,由n1n-1条双向道路连接。网络是连通的,任意一对互通立交之间有且仅有一条最短路径。第ii条道路连接互通uiu_iviv_i,长度为lil_i

可以建造恰好kk个带加油站的休息区,每个位于一个互通。司机可以从任意互通开始旅行到任何其他互通,始终沿着唯一最短路径。他们每次旅行都会带满油箱,只能在设有休息区的互通处加油。

亚历克斯想知道可能的最小燃油箱容量dd,以便可以以一种方式放置这kk个休息区,确保没有任何司机会耗尽燃油。在任何旅行中,司机永远不应该在沿途行驶超过dd个单位而不经过休息区,包括旅程的开始或结束。目标是找出最小的dd,假设休息区被以最佳方式放置。

思路讲解

https://hackmd.io/@tmt514/Bk_lRqjill#Problem-J—Gas-Station

说白了,只要每一颗子树都最小化加油站数量,然后我们再检测在最小化加油站数量前提下,是否违反了限制

贪心的关键是,找到最优子结构。最优子结构是什么?就是在不在这贪取决于什么!

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
auto dfs=[&](auto && self,ll u,ll p) -> ll {
ll dis=0;
ll plus=0;
for (auto [v,w]:g[u]) {
if (v==p) continue;
ll ldis=self(self,v,u);
ldis+=w;
// 如果说 ldis 本身就大于 d 这个门槛呢?那么给 v 节点加一个加油站
if (ldis>d_threshold) {
cnt_put++;
ldis=w;
}
// 如果说,ldis 和 最远子节点距离 (mx)dis 之间大于了 d 呢?
// 给 u 节点加一个加油站
if (ldis+dis>d_threshold) {
plus=1;
}
dis=max(dis,ldis);
}
cnt_put+=plus;
if (plus) {
return 0;
}
return dis;
};

AC代码

AC
https://codeforces.com/gym/106084/submission/361152548

心路历程(WA,TLE,MLE……)