补题链接:https://codeforces.com/gym/105459 或 https://qoj.ac/contest/1817
赛时最终成绩如下。在 A 题和 J 题上浪费了比较多的时间,否则应该还可以写出一道计算几何 B 题。不过就算早早地多过一道B题,看这罚时也是拿不到金牌的。
笑点解析:计算几何一眼就出了思路,但是最终都没有写;开场的时候不知道为什么判断“D题可做,不过不是签到题”,结果最后 D 题只有四个队伍 AC 。
下面的题目按照赛时 AC 顺序和我的赛后补题顺序排序。
M 奇怪的上取整
阅读理解题。对于一个单独的 f ( n , i ) f(n,i) f(n,i) ,函数会返回大于等于 n i \frac{n}{i} in 的最小的 n n n 的约数。因此每次 O ( n ) O(\sqrt{n}) O(n) 枚举出 n n n 的所有约数即可。对于 n n n 的两个相邻约数 u , v u, v u,v ( u < v ) (u < v) (u<v) ,可以使 f ( n , i ) f(n, i) f(n,i) 返回 v v v 的 i i i 有 n u − n v \frac{n}{u} - \frac{n}{v} un−vn 个。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) x.begin(),x.end()
typedef long long i64;
typedef pair<int, int> pii;
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T; cin >> T;
while(T--) {
i64 n, ans = 1; cin >> n;
vector<int> v1, v2;
for(int i = 1; i * i <= n; i++) {
if(n % i == 0) {
v1.push_back(i);
if(i * i != n) v2.push_back(n / i);
}
}
reverse(all(v2));
for(int x : v2) v1.push_back(x);
for(int i = 1; i < v1.size(); i++) {
ans += ((n / v1[i - 1]) - (n / v1[i])) * v1[i];
}
cout << ans << endl;
}
return 0;
}
C 在哈尔滨指路
场上是我写的,不幸地贡献了第一发罚时。按照题意直接模拟或者计算出终点直接走过去都可以,如果是后者需要特判走到起点的情况,因为不允许输出 0 0 0 步。我本以为前者打表会很麻烦,所以写的是后者,有个细节写错了 WA 了一发,但实际上前者更好写、更短、更不易错。下面代码是前者写法。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) x.begin(),x.end()
typedef long long i64;
typedef pair<int, int> pii;
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T; cin >> T;
while(T--) {
int n; cin >> n;
char op[10]; int dis[10];
for(int i = 0; i < n; i++)
cin >> op[i] >> dis[i];
cout << 2 * n - 1 << ' ' << op[0] << '\n';
cout << "Z " << dis[0] << '\n';
for(int i = 1; i < n; i++) {
if(op[i - 1] == 'N' && op[i] == 'W' ||
op[i - 1] == 'S' && op[i] == 'E' ||
op[i - 1] == 'W' && op[i] == 'S' ||
op[i - 1] == 'E' && op[i] == 'N')
cout << "L\n";
else cout << "R\n";
cout << "Z " << dis[i] << '\n';
}
}
return 0;
}
G 欢迎加入线上会议!
这题我完全没有参与,都是队友写的。要找到一张简单图的生成树,但是指定一些忙碌的点,这些点在生成树中度数为 1 1 1 并且必须是叶子。可以发现每个忙的点的父亲肯定是一个不忙的点。我们只保留连接两个不忙的点的边,跑出生成树(不连通就没有合法方案),然后将每个忙的点连到这棵树上即可(若连不上去也就没有合法方案)。我的代码写得有些繁琐,感觉现场的时候我的队友应该不会写得这么唐。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) x.begin(),x.end()
typedef long long i64;
typedef pair<int, int> pii;
const int MAXN = 2e5 + 10;
bool busy[MAXN];
vector<int> edge[MAXN], son[MAXN];
int fa[MAXN], sz[MAXN], ecnt, out;
int find(int x) {
if(fa[x] == x) return x;
else return fa[x] = find(fa[x]);
}
void merge(int u, int v) {
int tu = find(u);
int tv = find(v);
if(tu != tv) {
if(sz[tu] > sz[tv]) swap(tu, tv);
fa[tu] = tv; sz[tv] += sz[tu];
edge[u].push_back(v);
edge[v].push_back(u);
ecnt++;
}
}
void dfs(int u, int f) {
for(int x : edge[u]) {
if(x != f) {
son[u].push_back(x);
dfs(x, u);
}
}
if(!son[u].empty()) out++;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int n, m, k; cin >> n >> m >> k;
for(int i = 1; i <= n; i++) {
fa[i] = i; sz[i] = 1;
}
for(int i = 0; i < k; i++) {
int x; cin >> x;
busy[x] = true;
}
vector<pii> vec;
for(int i = 0; i < m; i++) {
int u, v; cin >> u >> v;
if(busy[u] && busy[v]) continue;
if(busy[u]) swap(u, v);
if(busy[v]) {
if(edge[v].empty()) {
edge[v].push_back(u);
edge[u].push_back(v);
ecnt++;
}
}
else vec.push_back({u, v});
}
for(pii p : vec) {
merge(p.first, p.second);
}
if(ecnt != n - 1) {
cout << "No" << endl;
return 0;
}
queue<int> q;
for(int i = 1; i <= n; i++) {
if(!busy[i]) {dfs(i, 0); q.push(i); break;}
}
cout << "Yes" << endl;
cout << out << endl;
while(!q.empty()) {
int f = q.front(); q.pop();
if(!son[f].empty()) {
cout << f << ' ' << son[f].size();
for(int x : son[f]) {
cout << ' ' << x;
q.push(x);
}
cout << endl;
}
}
return 0;
}
K 农场经营
这一题我又是完全没有参与,因为当时有几个队伍迅速切掉了 E 题,队友让我去开 E 了。过了一段时间发现 E 不会做,其他队过得早是可能因为他们太强了。回头一看发现队友说着二分啊树状数组啊就把题目过了。
自己写了一发才发现可以不用写树状数组,不过这个 n ≤ 1 0 5 n \le 10^5 n≤105 ,复杂度一个 log \log log 还是两个 log \log log 都无所谓啦。首先把输入数据按照单位价值 w w w 排序,预处理出一些信息,接着枚举选择哪种作物删除时间限制。取消某一作物时间限制后,首先需要把所有限制的下限干完,然后去干那些单位价值高的工作。二分出剩下的时间可以达到多少个高价值工作的上限,然后计算出实际获得的价值即可更新答案。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) x.begin(),x.end()
typedef long long i64;
typedef pair<int, int> pii;
const int N = 1e5 + 10;
i64 n, m, sumL, sumLw, ans;
struct STRU {
i64 L, R, w;
} a[N];
i64 pp[N], pw[N];
// pp : R - L 的前缀和
// pw : (R - L) * w 的前缀和
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> a[i].w >> a[i].L >> a[i].R;
}
sort(a + 1, a + n + 1, [](const STRU &A, const STRU &B){
return A.w < B.w;
});
for(int i = 1; i <= n; i++) {
sumL += a[i].L; sumLw += a[i].L * a[i].w;
pp[i] = pp[i - 1] + (a[i].R - a[i].L);
pw[i] = pw[i - 1] + (a[i].R - a[i].L) * a[i].w;
}
for(int i = 1; i <= n; i++) {
sumL -= a[i].L; sumLw -= a[i].L * a[i].w;
i64 lastt = m - sumL;
int L = i, R = n, mid;
while(L < R) {
mid = (L + R + 1) / 2;
if(pp[n] - pp[mid - 1] > lastt) L = mid;
else R = mid - 1;
}
i64 tans = sumLw + pw[n] - pw[L];
tans += (lastt - (pp[n] - pp[L])) * a[L].w;
ans = max(ans, tans);
sumL += a[i].L; sumLw += a[i].L * a[i].w;
}
cout << ans << endl;
return 0;
}
J 新能源汽车
这题一开始就有很多队伍做了出来,有点歪榜的意味。队友开始的时候给出了一个假的做法,写完代码细细检查后还是没敢提交,事后证明决定是正确的(没搞懂他们两个怎么能够忍住不交的)。我后来提出一个正确的做法(也是出题人做法)然后过了。
这题其实就是贪心:选择消耗的电瓶肯定是还有电的电瓶中,充电站最近的那个。拿上两个堆维护即可——一个是还有电的集合,一个是待充电的集合,堆内排序的依据就是下一个充电站的位置,充电站最近的在堆顶。在走到下一个充电站之前,不断拿出堆顶的电池使用,一旦用完就放到待充电集合中;到充电站时,对应的电池一定在某个堆的堆顶,拿出来,充满,更新充电站位置,塞回有电集合即可。注意到有些充电站的位置相同,那就规定电瓶索引小的在前面(我的这个写法好像怎么规定都行,因为我的堆内排序依据是排序后的充电站索引,不会出现重复)。 我们最多从有电的那个堆中取出
n
+
m
n + m
n+m 次堆顶,因此时间复杂度
O
(
(
n
+
m
)
log
n
)
O((n + m) \log n)
O((n+m)logn) ,没有问题。
顺便一题,赛场上队友还帮忙出了几个样例,它们对这道题的快速调出贡献颇巨,样例如下:
Update:评论区有人提醒题目保证充电站位置互不相同且单调递增,因此这里面有几个测试用例是不合法的。
4
3 2
5 5 5
3 1
4 1
2 3
5 5
2 1
3 2
4 1
2 2
5 5
10 1
10 2
2 2
5 5
8 1
8 2
答案为
19
14
20
18
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) x.begin(),x.end()
typedef long long i64;
typedef pair<i64, i64> pii;
const int N = 1e5 + 10;
queue<int> q[N];
i64 a[N]; pii st[N];
struct stru {i64 last, nextst, index;};
bool operator < (const stru &A, const stru &B) {
return A.nextst > B.nextst;
}
priority_queue<stru> q1, q2;
void solve() {
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
while(!q[i].empty()) q[i].pop();
while(!q1.empty()) q1.pop();
while(!q2.empty()) q2.pop();
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= m; i++)
cin >> st[i].first >> st[i].second;
sort(st + 1, st + m + 1);
for(int i = 1; i <= m; i++)
q[st[i].second].push(i);
for(int i = 1; i <= n; i++)
q[i].push(m + 1);
for(int i = 1; i <= n; i++)
q1.push({a[i], q[i].front(), i});
i64 dis = 0;
for(int i = 1; i <= m; i++) {
while(!q1.empty() && dis < st[i].first) {
stru t = q1.top(); q1.pop();
if(t.last + dis > st[i].first) {
t.last -= st[i].first - dis;
dis = st[i].first;
q1.push(t);
} else {
dis += t.last; t.last = 0;
q2.push(t);
}
}
if(q1.empty() && dis < st[i].first) {
cout << dis << endl; return;
}
if(!q1.empty() && q1.top().nextst == i) {
stru t = q1.top(); q1.pop();
q[t.index].pop();
t.nextst = q[t.index].front();
t.last = a[t.index];
q1.push(t);
} else if(!q2.empty() && q2.top().nextst == i) {
stru t = q2.top(); q2.pop();
q[t.index].pop();
t.nextst = q[t.index].front();
t.last = a[t.index];
q1.push(t);
}
}
while(!q1.empty()) {
dis += q1.top().last;
q1.pop();
}
cout << dis << endl;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T; cin >> T;
while(T--) solve();
return 0;
}
L 树上游戏
都是队友写的,我没参与。赛时队友的状态就是写了一发,交上去 WA 了,打印代码,到旁边 Debug ;每过几分钟就“我又想到我漏了什么了”,改一发再交再 WA 再打印。这个过程中我一直在写 A 题,整个队伍都处于一种非常红温的状态。好在队友在 4.5 4.5 4.5 小时的时候过了,于是红温全部转移给我。
这道题我就直接抄题解了。首先需要计算出所有的
X
2
X^2
X2 的和。考虑两条路径的公共边
e
1
,
e
2
,
…
,
e
k
e_1, e_2, \dots, e_k
e1,e2,…,ek ,则对答案的贡献就是
(
∣
e
1
∣
+
∣
e
2
∣
+
⋯
+
∣
e
k
∣
)
2
=
∑
i
=
1
k
∣
e
i
∣
2
+
∑
1
≤
i
,
j
≤
k
,
i
≠
j
∣
e
i
∣
⋅
∣
e
j
∣
(|e_1| + |e_2| + \cdots + |e_k|) ^2 = \sum\limits_{i=1}^k |e_i|^2 + \sum\limits_{1 \le i, j \le k, i \ne j} |e_i| \cdot |e_j|
(∣e1∣+∣e2∣+⋯+∣ek∣)2=i=1∑k∣ei∣2+1≤i,j≤k,i=j∑∣ei∣⋅∣ej∣
因此一种选法的贡献可以分成两种:一种由公共边
e
i
e_i
ei 产生,一种由边的有序对
(
e
i
,
e
j
)
(e_i, e_j)
(ei,ej) 产生。两种贡献不能合并,因为
(
e
i
,
e
j
)
(e_i, e_j)
(ei,ej) 的贡献和
(
e
j
,
e
i
)
(e_j, e_i)
(ej,ei) 的贡献需要分别计算,但
(
e
i
,
e
i
)
(e_i, e_i)
(ei,ei) 的贡献(即
∣
e
i
∣
2
|e_i|^2
∣ei∣2 )不会算第二遍。
整棵树随便选一个点作为根节点,并定义 s u s_u su 为以 u u u 为根节点的子树的大小, t u t_u tu 为以 u u u 为根节点的子树上所有的 s u 2 s_u^2 su2 的和,即 t u = ∑ v ∈ s u b t r e e ( u ) s v 2 t_u = \sum\limits_{v \in \mathrm{subtree}(u)} s_v^2 tu=v∈subtree(u)∑sv2 。
对于第一种贡献,我们发现,只要双方选择的路径端点都在某条边 e i e_i ei 的两侧, e i e_i ei 就会做出贡献;我们只在 e i e_i ei 的儿子侧对答案顺便进行统计,设其儿子侧是 u u u ,则一方选取到这条边的总情况就是 s u ⋅ ( n − s u ) s_u \cdot (n - s_u) su⋅(n−su) ;这条边的贡献,就是双方都选到的总情况就是 s u 2 ⋅ ( n − s u ) 2 s_u^2 \cdot (n - s_u)^2 su2⋅(n−su)2 。注意到根节点没有父亲边,好在在根节点处对答案的修改量是 n 2 ⋅ ( n − n ) 2 = 0 n^2 \cdot (n - n)^2 = 0 n2⋅(n−n)2=0 ,不用进行特殊判断。
对于第二种贡献,可以发现,将两条边连上,形成的这条链的两端外的点的数量的乘积的平方就是这两条边形成的有序对的贡献值。考虑在点 u u u 处只计算最近公共祖先为 u u u 的边有序对 ( e i , e j ) (e_i, e_j) (ei,ej) 的贡献。分两种情况讨论:
- e i , e j e_i, e_j ei,ej 在 u u u 的同一个分支上。由于 e i , e j e_i, e_j ei,ej 的最近公共祖先是 u u u ,因此肯定有一条边是 ( u , v ) (u, v) (u,v) ,其中 v v v 是 u u u 的直接儿子。两条边连成链,链外面的点分别是 u u u 向上的部分和较深的那条边向下的部分,后者是 v v v 的子树中除 v v v 之外的点的 s i 2 s_i^2 si2 的和,即 t v − s v 2 t_v - s_v^2 tv−sv2 ,因此对于 u u u 的每个儿子 v v v ,都会对答案有 2 ⋅ ( n − s v ) 2 ⋅ ( t v − s v 2 ) 2 \cdot (n - s_v)^2 \cdot (t_v - s_v^2) 2⋅(n−sv)2⋅(tv−sv2) 的贡献。前面这个系数 2 2 2 是因为两条边都可以是 ( u , v ) (u, v) (u,v) 。如果这个分支只有一条边,式子中的 t v − s v 2 = 0 t_v - s_v^2 = 0 tv−sv2=0 ,不会有问题。
-
e
i
,
e
j
e_i, e_j
ei,ej 在
u
u
u 的不同分支上。假设两条边的比较深的点分别是
x
,
y
x, y
x,y ,则连接两条边形成的链就是
x
,
y
x, y
x,y 的路径,对答案的贡献值就是
s
x
2
⋅
s
y
2
s_x^2 \cdot s_y^2
sx2⋅sy2 。我们在
u
u
u 这里需要统计的对答案的贡献就是下式。如果
u
u
u 只有一个儿子,最终的式子就是
t
v
2
−
t
v
2
=
0
t_v^2 - t_v^2 = 0
tv2−tv2=0 ,不会有问题。
∑ v 1 , v 2 ∈ s o n ( u ) v 1 ≠ v 2 ∑ x ∈ s u b t r e e ( v 1 ) y ∈ s u b t r e e ( v 2 ) s x 2 ⋅ s y 2 = ∑ v 1 , v 2 ∈ s o n ( u ) v 1 ≠ v 2 t v 2 ⋅ t v 1 = ( ∑ v ∈ s o n ( u ) t v ) 2 − ∑ v ∈ s o n ( u ) t v 2 \begin{aligned} & \sum\limits_{\substack{v_1, v_2 \in \mathrm{son}(u) \\ v_1 \ne v_2}} \,\, \sum\limits_{\substack{x \in \mathrm{subtree}(v_1) \\ y \in \mathrm{subtree}(v_2)}} s_x^2 \cdot s_y^2 \\ = & \sum\limits_{\substack{v_1, v_2 \in \mathrm{son}(u) \\ v_1 \ne v_2}} t_{v_2} \cdot t_{v_1} \\ = & \Bigg(\sum\limits_{v \in \mathrm{son}(u)} t_v\Bigg)^2 - \sum\limits_{v \in \mathrm{son}(u)} t_v^2 \end{aligned} ==v1,v2∈son(u)v1=v2∑x∈subtree(v1)y∈subtree(v2)∑sx2⋅sy2v1,v2∈son(u)v1=v2∑tv2⋅tv1(v∈son(u)∑tv)2−v∈son(u)∑tv2
上述的所有统计都可以在一次 DFS 中跑出来,时空复杂度都是 O ( n ) O(n) O(n) 的。最后别忘记乘上 [ n 2 ( n − 1 ) 2 2 2 ] − 1 \left[\frac{n^2(n - 1)^2}{2^2}\right]^{-1} [22n2(n−1)2]−1 。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) x.begin(),x.end()
typedef long long i64;
typedef pair<int, int> pii;
const int N = 1e5 + 10;
const int mod = 998244353;
vector<int> edge[N];
i64 s[N], t[N], ans, n;
void DFS(int u, int f) {
s[u] = 1; i64 st = 0, st2 = 0;
for(int v : edge[u]) {
if(v == f) continue;
DFS(v, u); s[u] += s[v];
t[u] = (t[u] + t[v]) % mod;
ans += 2LL * (n - s[v]) % mod * (n - s[v]) % mod * ((t[v] - s[v] * s[v] % mod + mod) % mod) % mod;
st = (st + t[v]) % mod; st2 = (st2 + t[v] * t[v] % mod) % mod;
}
t[u] = (t[u] + s[u] * s[u]) % mod;
ans += s[u] * s[u] % mod * (n - s[u]) % mod * (n - s[u]) % mod;
ans += (st * st % mod - st2 + mod) % mod; ans %= mod;
}
i64 inv(i64 x) {
i64 ret = 1, p = mod - 2;
while(p) {
if(p & 1) ret = ret * x % mod;
x = x * x % mod; p >>= 1;
}
return ret;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T; cin >> T;
while(T--) {
cin >> n; ans = 0;
for(int i = 1; i <= n; i++)
edge[i].clear(), s[i] = t[i] = 0;
for(int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
edge[u].push_back(v);
edge[v].push_back(u);
}
DFS(1, 0);
ans = ans * inv(n - 1) % mod * inv(n - 1) % mod;
ans = ans * inv(n) % mod * inv(n) % mod * 4LL % mod;
cout << ans << endl;
}
return 0;
}
A 造计算机
最后一小时让我持续红温的一道题。一直写一直改到 4:50 才交上第一发,此时代码已经来到了两百多行。交上去一看 RE 了,队友盯了两分钟发现原因是有返回值的函数没有安排一个变量用于接收返回值,我之前没有遇过这种情况,这下学到了。再交一发,意料之中(?)地 WA 了,这时候我已经失去了心气,懒洋洋地百无聊赖地聊天 + 测试数据,测试发现 L = 6 , R = 7 L = 6, R = 7 L=6,R=7 的时候有一个点伸出去了两条完全相同的边,于是在输出前把 vector 排序去重,不抱希望地交了一发,没想到意外地过了。这是本场比赛倒数第十二个AC提交,当时我就没忍住“卧槽”了一声。因为极限AC被队友推去当领奖人了,高兴。
这题的思路还是比较多样的,我们的思路来自于队友的一句“想到了数位DP的卡上界方法,分成小于上界的和目前为止等于上界的两部分”。那时候我们已经发现了下图所示的结构:
这个结构中, i i i 个纺锤形就可以表示出 [ 0 , 2 i − 1 ] [0, 2^i - 1] [0,2i−1] 的所有整数。因此思路很快就出来了:
如果
L
L
L 和
R
R
R 的二进制表示位数不同,就从头到尾连两条链分别表示
L
L
L 和
R
R
R ,假设
L
L
L 的二进制表示是
x
x
x 位,
R
R
R 的二进制表示是
y
y
y 位,介于
x
x
x 位和
y
y
y 位之间的数就可以从起始点连一条线到纺锤串上;
L
L
L 链条中,数位为
0
0
0 的边需要连一条平行的权值为
1
1
1 的边到纺锤串上;
R
R
R 链条中,数位为
1
1
1 的边需要连一条平行的权值为
0
0
0 的边到纺锤串上(但第一条边不需要连平行边)。如此操作最多使用大约
60
60
60 个点。下面是
L
=
5
=
(
101
)
2
,
R
=
41
=
(
101001
)
2
L = 5 = (101)_2, R = 41 = (101001)_2
L=5=(101)2,R=41=(101001)2 时的图,下边缘是
L
=
(
101
)
2
L = (101)_2
L=(101)2 ,上边缘是
R
=
(
101001
)
2
R = (101001)_2
R=(101001)2 。
如果
L
L
L 和
R
R
R 的二进制表示有着相同的位数,那在它们的第一处相异之前,两者有着相同的路线。在第一处相异处,分叉;在分叉之后,
L
L
L 链条中,数位为
0
0
0 的边需要连一条平行的权值为
1
1
1 的边到纺锤串上;
R
R
R 链条中,数位为
1
1
1 的边需要连一条平行的权值为
0
0
0 的边到纺锤串上。如此操作最多使用大约
60
60
60 个点。下面是
L
=
37
=
(
100101
)
2
,
R
=
45
=
(
101101
)
2
L = 37 = (100101)_2, R = 45 = (101101)_2
L=37=(100101)2,R=45=(101101)2 时的图,下边缘是
L
=
(
100101
)
2
L = (100101)_2
L=(100101)2 ,上边缘是
R
=
(
101101
)
2
R = (101101)_2
R=(101101)2 。
我认为那个纺锤串很重要,所以令终点为 0 0 0 号点,距离终点 i i i 个纺锤的点为 − i -i −i 号点,先这么写,最后将纺锤串和终点赋上编号,生成纺锤串。这份代码比较啰嗦,感觉主要是因为构造方式不够巧妙。赛时代码在第二种情况的分叉处有一点细节上的问题,去重就可以过,不过下面这份代码没有问题,不需要做这项额外的修正。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) x.begin(),x.end()
typedef long long i64;
typedef pair<int, int> pii;
int L, R, cnt, m2p[30];
vector<pii> edge[110];
// To binary string
string tobs(int x) {
string ret;
while(x) {
ret.push_back('0' + (x & 1));
x >>= 1;
}
reverse(all(ret));
return ret;
}
int main() {
cin >> L >> R;
string sL = tobs(L), sR = tobs(R);
if(sL.length() != sR.length()) {
int start = ++cnt; // start = 1
for(int i = sL.length() + 1; i < sR.length(); i++) {
edge[1].push_back({-(i - 1), 1});
}
// L chain
for(int i = 0, f = start; i < sL.length(); i++) {
if(i + 1 == sL.length()) {
edge[f].push_back({0, sL[i] - '0'});
if(i != 0 && sL[i] == '0') edge[f].push_back({0, 1});
} else {
edge[f].push_back({++cnt, sL[i] - '0'});
if(i != 0 && sL[i] == '0') edge[f].push_back({i - (int)sL.length() + 1, 1});
f = cnt;
}
}
// R chain
for(int i = 0, f = start; i < sR.length(); i++) {
if(i + 1 == sR.length()) {
edge[f].push_back({0, sR[i] - '0'});
if(i != 0 && sR[i] == '1') edge[f].push_back({0, 0});
} else {
edge[f].push_back({++cnt, sR[i] - '0'});
if(i != 0 && sR[i] == '1') edge[f].push_back({i - (int)sR.length() + 1, 0});
f = cnt;
}
}
} else {
int pos = 100, start = ++cnt;
for(int i = 0; i < sL.length(); i++) {
if(sL[i] != sR[i]) {pos = i; break;}
}
for(int i = 0, pL = start, pR = start; i < sL.length(); i++) {
// same : first half
if(i < pos) {
if(i + 1 == sL.length()) {
edge[pL].push_back({0, sL[i] - '0'});
} else {
edge[pL].push_back({++cnt, sL[i] - '0'});
pL = pR = cnt;
}
}
// after : second half
else {
if(i + 1 == sL.length()) {
edge[pL].push_back({0, sL[i] - '0'});
if(i > pos && sL[i] == '0') edge[pL].push_back({0, 1});
edge[pR].push_back({0, sR[i] - '0'});
if(i > pos && sR[i] == '1') edge[pR].push_back({0, 0});
} else {
edge[pL].push_back({++cnt, sL[i] - '0'});
if(i > pos && sL[i] == '0') edge[pL].push_back({i - (int)sL.length() + 1, 1});
pL = cnt;
edge[pR].push_back({++cnt, sR[i] - '0'});
if(i > pos && sR[i] == '1') edge[pR].push_back({i - (int)sR.length() + 1, 0});
pR = cnt;
}
}
}
}
int minp = 0;
for(int i = 1; i <= cnt; i++) {
for(auto p : edge[i]) minp = min(minp, p.first);
}
for(int i = 0; i >= minp; i--) {
m2p[-i] = ++cnt;
if(i != 0) {
edge[m2p[-i]].push_back({m2p[-(i + 1)], 0});
edge[m2p[-i]].push_back({m2p[-(i + 1)], 1});
}
}
cout << cnt << endl;
for(int i = 1; i <= cnt; i++) {
cout << edge[i].size();
for(auto p : edge[i]) {
if(p.first <= 0) cout << ' ' << m2p[-p.first];
else cout << ' ' << p.first;
cout << ' ' << p.second;
}
cout << endl;
}
return 0;
}
B 凹包
2023ICPC西安站的一个巨大的贡献是,催生了许多“凹包”题目的出现(乐)。
这道题目有一个非常重要的提示就是,“凸多边形中任意两点间的线段上的所有点都在多边形内”,这启示我们将凸包内的点与凸包上的点相连来构造凹包。发现可以先求出一个凸包,然后选择凸包上相邻的两个点 u , v u, v u,v ,与凸包内的一个点 w w w 连线,就可以得到一个面积尽可能大的凹包,面积是 S c o n v e x h u l l − S △ u v w S_{\mathrm{convex \,hull}}- S_{\triangle uvw} Sconvexhull−S△uvw 。为了最小化 S △ u v w S_{\triangle uvw} S△uvw ,我们需要对凸包上的每一条边 ( u , v ) (u, v) (u,v) 找到距离最近的点 w w w 。想象一个凸包外的直线,按照顺序遍历凸包上的顶点,顶点和直线的距离是单调减或单调增的,因此我们求出最外面凸包后,在内部再求一个凸包,双指针遍历内外凸包,每次都最小化内凸包顶点与外凸包的边的距离即可。求凸包复杂度 O ( n log n ) O(n \log n) O(nlogn) ,双指针复杂度 O ( n ) O(n) O(n) 。注意要特判内部点数量为 0 0 0 、 1 1 1 、 2 2 2 时的情况。
这里其实还有一个小问题:为什么是指定外凸包的边,寻找内凸包距离最近(等价于三角形面积最小)的点,而不是反过来指定内凸包的点,寻找使三角形面积最小的外凸包上的边?答案是后者中三角形的面积受到距离和边长双重因素影响,两者均不存在单调性,我构造了一个例子来更好地说明这一点:
比如说类似这样的情况,决定最终答案的三角形是 △ c C D \triangle cCD △cCD ,内凸包按照 a b c abc abc 的顺序遍历, a a a 对应最小的三角形是 △ a A B \triangle aAB △aAB , b b b 对应的最小的三角形是 △ b A B \triangle bAB △bAB ,到点 c c c 的时候,会因为 S △ B C c S_{\triangle BCc} S△BCc 过大,比 S △ A B c S_{\triangle ABc} S△ABc 还大,导致无法转移到全局最优解 △ c C D \triangle cCD △cCD 上。
#include<bits/stdc++.h>
using namespace std;
typedef long long i64;
struct point {
i64 x, y;
point(i64 x = 0, i64 y = 0): x(x), y(y) {}
i64 hval() {return ((x + 1000000000LL) << 31) | (y + 1000000000LL);}
};
struct arrow {
i64 x, y;
arrow(i64 x = 0, i64 y = 0): x(x), y(y) {}
arrow(point a, point b): x(b.x - a.x), y(b.y - a.y) {}
};
bool operator == (const point &a, const point &b) {
return a.x == b.x && a.y == b.y;
}
bool operator < (const point &a, const point &b) {
if(a.x == b.x) return a.y < b.y;
else return a.x < b.x;
}
i64 dot(const arrow &a, const arrow &b) {
return a.x * b.x + a.y * b.y;
}
i64 cross(const arrow &a, const arrow &b) {
return a.x * b.y - a.y * b.x;
}
// 三角形面积的两倍
i64 area(const point &a, const point &b, const point &c) {
return abs(cross({a, b}, {a, c}));
}
// 将参数 v 变成一个凸包,逆时针排序,v[0] 是坐标最小的点
void makeConvex(vector<point> &v) {
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
vector<point> t;
for(int cnt = 0; cnt < 2; cnt++) {
reverse(v.begin(), v.end());
stack<point> p; stack<arrow> e;
p.push(v[0]);
for(int i = 1; i < v.size(); i++) {
// 下面这个 while 会导致凸包上任意三个点都不在同一个直线上
// 如果需要保留凸包边中间的点,把 >= 改成 > 即可
while(!e.empty() && cross(e.top(), {p.top(), v[i]}) >= 0)
p.pop(), e.pop();
e.push(arrow(p.top(), v[i]));
p.push(v[i]);
}
while(p.size() > 1)
t.push_back(p.top()), p.pop();
}
v = t;
}
void solve() {
i64 n, ans = 0, mint = 8e18; cin >> n;
vector<point> v, t, temp;
unordered_set<i64> mp;
for(int i = 1; i <= n; i++) {
i64 x, y; cin >> x >> y;
v.push_back({x, y});
temp.push_back({x, y});
}
makeConvex(v); v.push_back(v[0]);
for(int i = 1; i < v.size(); i++) {
ans += area(v[0], v[i], v[i - 1]);
}
for(auto p : v) {
mp.insert(p.hval());
}
for(auto p : temp) {
if(!mp.count(p.hval())) t.push_back(p);
}
if(t.size() == 0) {cout << "-1" << endl; return;}
else if(t.size() < 3) {
for(auto p : t) for(int i = 1; i < v.size(); i++)
mint = min(mint, area(p, v[i], v[i - 1]));
cout << ans - mint << endl;
} else {
makeConvex(t); int szt = t.size(), p = 0;
for(int i = 0; i < szt * 2; i++) {
t.push_back(t[i]);
}
for(int i = 0; i < szt; i++) {
if(area(v[0], v[1], t[i]) < area(v[0], v[1], t[p])) p = i;
}
for(int i = 1; i < v.size(); i++) {
while(area(v[i], v[i - 1], t[p + 1]) <= area(v[i], v[i - 1], t[p])) p++;
mint = min(mint, area(v[i], v[i - 1], t[p]));
}
cout << ans - mint << endl;
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int T; cin >> T;
while(T--) solve();
return 0;
}
E 弹珠赛跑
赛场上呆呆鸟切这题切得太快了,队友让我去开这一题,我很快想到了 O ( n 4 ) O(n^4) O(n4) 的解法,但是复杂度降不下去了。 O ( n 4 ) O(n^4) O(n4) 的解法很朴素:由于 m m m 是奇数,中位数位置肯定是某个球的位置。枚举是哪个球、从哪个位置出发,当它到达原点的时候,有一半的球在它前面。枚举是 O ( n m ) O(nm) O(nm) 的,求得一半的球在它前面的概率是一个 O ( m 2 ) O(m^2) O(m2) 的背包问题,因此总复杂度 O ( n m 3 ) O(nm^3) O(nm3) 。
正解就是在上述的思路的基础上做了一个回退背包。回退背包的思想是:背包问题与物品的求解顺序无关,因此每一个物品都可以认为是最后一个被求解的,那就可以对着 DP 的式子逆向操作,退回这个物品的 DP 状态。我们要求总共 n m nm nm 次背包问题,如果相邻两次背包之间只有 x x x 个物品的状态不同,那我们就可以把这 x x x 个物品的 DP 状态回退,进行修改,然后再加回去。
来看一下单次背包的式子。假设我们指定的那个球那个起始点,到原点的时间为 t t t ,其它的第 i i i 个球有 p i p_i pi 个起始点可以在不晚于 t t t 的时间到达原点,则背包的 DP 初始化是 d p [ 0 ] = 1 \mathrm{dp}[0] = 1 dp[0]=1 , d p [ i ] = 0 ( 1 ≤ i ≤ m ) \mathrm{dp}[i] = 0 (1 \le i \le m) dp[i]=0(1≤i≤m) ,第 i i i 个物品加入 DP 状态就是使 j : m − 1 → 0 j : m - 1 \rightarrow 0 j:m−1→0 , d p [ j + 1 ] = d p [ j + 1 ] + p i n d p [ j ] \mathrm{dp}[j + 1] = \mathrm{dp}[j + 1] + \frac{p_i}{n} \mathrm{dp}[j] dp[j+1]=dp[j+1]+npidp[j] , d p [ j ] = n − p i n d p [ j ] \mathrm{dp}[j] = \frac{n - p_i}{n}\mathrm{dp}[j] dp[j]=nn−pidp[j] 。若要回退这个操作,就是使 j : 0 → m − 1 j : 0 \rightarrow m - 1 j:0→m−1 , d p [ j ] = n n − p i d p [ j ] \mathrm{dp}[j] = \frac{n}{n - p_i}\mathrm{dp}[j] dp[j]=n−pindp[j] , d p [ j + 1 ] = d p [ j + 1 ] − p i n d p [ j ] \mathrm{dp}[j + 1] = \mathrm{dp}[j + 1] - \frac{p_i}{n} \mathrm{dp}[j] dp[j+1]=dp[j+1]−npidp[j] 。可以发现,在这个抽象出的背包问题中,一个物品的参数就只有 p i p_i pi ,只要 p i p_i pi 不变就不用回退。
按照时间 t t t 对 n m nm nm 个 (球,出发点) pair 进行排序,排序后,相邻两个背包间,只有一个球的 p i p_i pi 发生了改变,就是当前球的 p i p_i pi 增加了 1 1 1 。因此需要把这个球的状态回退,更新答案与 p i p_i pi ,再将状态 DP 回去。
这题的一部分数据的逆需要预处理,否则 O ( n 3 log m o d ) O(n^3 \log \mathrm{mod}) O(n3logmod) 会超时。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) x.begin(),x.end()
typedef long long i64;
typedef pair<i64, i64> pii;
const int mod = 1000000007;
i64 inv(i64 x) {
i64 ret = 1, p = mod - 2;
while(p) {
if(p & 1) ret = ret * x % mod;
x = x * x % mod; p >>= 1;
} return ret;
}
i64 n, m, x[505], v[505], invv[505], invi[505];
i64 dp[505], pi[505];
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> x[i];
invi[i] = inv(i);
}
for(int i = 1; i <= m; i++) {
cin >> v[i];
invv[i] = inv(v[i]);
}
vector<pii> t;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
t.push_back({j, i});
sort(all(t), [](const pii &A, const pii &B){
// tA < tB : -x[A.second] * v[B.first] < -x[B.second] * v[A.first]
return -x[A.second] * v[B.first] < -x[B.second] * v[A.first];
});
dp[0] = 1; i64 ans = 0;
for(auto p : t) {
int ball = p.first, pos = p.second;
for(int i = 0; i < m; i++) {
dp[i] = dp[i] * n % mod * invi[n - pi[ball]] % mod;
dp[i + 1] = (dp[i + 1] - dp[i] * pi[ball] % mod * invi[n] % mod + mod) % mod;
}
i64 time = (-x[pos]) * invv[ball] % mod;
ans = (ans + dp[m / 2] * time % mod) % mod;
pi[ball] += 1;
for(int i = m - 1; i >= 0; i--) {
dp[i + 1] = (dp[i + 1] + dp[i] * pi[ball] % mod * invi[n] % mod) % mod;
dp[i] = dp[i] * (n - pi[ball]) % mod * invi[n] % mod;
}
}
cout << ans * invi[n] % mod << endl;
return 0;
}