第十届中国大学生程序设计竞赛 哈尔滨站(CCPC 2024 Harbin Site)
文章目录
- 第十届中国大学生程序设计竞赛 哈尔滨站(CCPC 2024 Harbin Site)
- The 3rd Universal Cup. Stage 14: Harbin
- 前言
- Problem C. Giving Directions in Harbin(签到)
- Problem G. Welcome to Join the Online Meeting! (简单图论)
- Problem M. Weird Ceiling(简单数学题)
- Problem B. Concave Hull(凸包,铜牌题)
- Problem K. Farm Management(简单数据结构,贪心)
- Problem J. New Energy Vehicle(贪心)
- Problem A. Build a Computer(位运算,图论,构造)
- Problem L. A Game On Tree(树形结构,数学期望,组合数学)
The 3rd Universal Cup. Stage 14: Harbin
本人博客园地址:博客搬家后本文地址
前言
- VP赛时 5 5 5 题(CGMKJ),赛后 8 8 8 题(+BAL)。(铜线5题,银线6题,金线8题)
- 签到题只提供思路,后面的一些题提供思路+代码。
Problem C. Giving Directions in Harbin(签到)
题目大意:在网格图上指路,你知道的是绝对位置,比如从这个路口往南走 2 个路口再往东走 1 个路口,你要输出一个相对序列, 比如面向南,沿街直走 2 个十字路口左拐再走一个十字路口。
题目解析:两种解法,一种是直接模拟,第二种是求出起点和终点直接按最短路走就行。
Problem G. Welcome to Join the Online Meeting! (简单图论)
题目大意:给出一张 n n n 个点的无向图(不一定连通),保证每个点的度 都大于等于 1。找出此图的一棵生成树,并满足给定的 k k k 个 节点的度为 1。
题目解析:将给定的 k k k 个点看作特殊点,在建图时,只保留非特殊点间的双向边和非特殊点连向特殊点的单向边。以一个非特殊点为起点搜一遍,若不连通,则无解,否则这棵搜索树就是所求的答案,实现注意一下细节。
Problem M. Weird Ceiling(简单数学题)
题目大意:给出了一个「上取整」程序 f ( a , b ) f(a, b) f(a,b),求 ∑ i = 1 n f ( n , i ) ∑^n_{i=1} f(n, i) ∑i=1nf(n,i) 。 数据范围: 1 ≤ n ≤ 1 0 9 1 ≤ n ≤ 10^9 1≤n≤109。
题目解析: 队友写的,题本身也算个签到,先贴个代码。
// Author: Chuanhua Yu
// Time: 2024-11-03
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using vi = vector<int>;
ll n;
void solve(){
cin >> n;
ll ans = 0;
if(n==1){
cout<<"1\n";
return;
}
vector<ll> ls;
ll tmp=sqrt(n);
for(ll i = 1; i <= tmp; i++){
if(n%i==0){
ll j=n/i;
ls.push_back(i);
if(j!=i)
ls.push_back(j);
}
}
sort(ls.begin(),ls.end());
for(int i=0;i<ls.size()-1;++i){
ans+=n/ls[i]*(ls[i+1]-ls[i]);
}
ans++;
cout<<ans<<'\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int T;
cin >> T;
while(T --)
solve();
return 0;
}
Problem B. Concave Hull(凸包,铜牌题)
题目大意: 二维平面上的点集,要求选择若干个点以及点之间的连接顺序,使得这些点连成一个面积严格 > 0 > 0 >0 的简单凹多边形,最 大化这个凹多边形的面积, n ≤ 1 0 5 n ≤ 10^5 n≤105。
题目解析:
- 求凹包的最大面积,可以理解为求凸包后,再增加一个点后的图形的面积
- 考虑把原凸包的点删掉后剩下的点怎么增加才能让面积最大
- 对于凸包的每一条边,距离边最近的点是最优的
- 而这个点,一定在剩下的点的凸包上
- 因此对剩下的点再求一遍凸包
- 两个图形间,外边和内点的最小距离,可以用双指针维护
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int sgn(ll x){return x==0?0:x>0?1:-1;}
struct point{
ll x,y,id;
point(ll x,ll y,int id):x(x),y(y),id(id){}
point operator-(point b){return point(x-b.x,y-b.y,0);}
ll operator*(point b){return x*b.y-y*b.x;}
bool operator<(point b){return sgn(x-b.x)<0||(sgn(x-b.x)==0&&sgn(y-b.y)<0);}
bool operator==(point b){return id==b.id;}
};
typedef vector<point> vp;
int convex_hull(vp &p,int n,vp &w){
vp t=p;
if(n<=2) {w=p;return n;}
int tw=unique(t.begin(),t.begin()+n)-t.begin();
sort(t.begin(),t.begin()+tw);
w.clear();
int v=0;
for(int i=0;i<tw;i++){
while(v>1&&sgn((w[v-1]-w[v-2])*(t[i]-w[v-2]))<=0) v--,w.pop_back();
w.push_back(t[i]);
v++;
}
int tp=v;
for(int i=tw-1;i>=0;i--){
while(v>tp&&sgn((w[v-1]-w[v-2])*(t[i]-w[v-2]))<=0) v--,w.pop_back();
w.push_back(t[i]);
v++;
}
if(tw>1) v--;
return v;
}
ll area(vp &a){
ll ins=0;
for(int i=0;i<a.size();i++) ins+=a[i]*a[(i+1)%a.size()];
return abs(ins);
}
void solve(){
vp p;
int n;
cin>>n;
for(int i=1,x,y;i<=n;i++) cin>>x>>y,p.push_back({x,y,i-1});
vector<point> w;
int t=convex_hull(p,n,w);
vp pt;
vector<int> vis(n+1,0);
for(int i=0;i<t;i++) vis[w[i].id]=1;
for(int i=0;i<n;i++) if(!vis[i]) pt.push_back(p[i]);
if(pt.size()==0){
cout<<-1<<'\n';
return;
}
vector<point> wt;
int tt=convex_hull(pt,pt.size(),wt);
int now=0;
ll dit=LLONG_MAX;
for(int i=0;i<t;i++){
while(1){
ll l=(w[(i+1)%t]-w[i])*(wt[now]-w[i]);
ll r=(w[(i+1)%t]-w[i])*(wt[(now+1)%tt]-w[i]);
dit=min({dit,l,r});
if(r>=l) break;
now=(now+1)%tt;
}
dit=min(dit,abs((w[i]-wt[now])*(w[(i+1)%t]-wt[now])));
}
cout<<area(w)-dit<<'\n';
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
int ins;
cin>>ins;
while(ins--) solve();
return 0;
}
Problem K. Farm Management(简单数据结构,贪心)
题目大意:有 n n n 种作物,每天工作恰好 m m m 个单位时间,对于每种作物, 处理一单位时间该种作物收益为 w i w_i wi,处理这种作物的工作 时长可以是 [ l i , r i ] [l_i , r_i] [li,ri] 中的一个整数。现在可以删除最多一种作物工作时长的限制,也就是处理这种作物的工作时长变为 [ 0 , + ∞ ) [0, +∞) [0,+∞) 中的一个整数,但要保证每天仍然恰好工作 m m m 小 时。问最大收益。
题目解析:首先对所有作物按 w i w_i wi 排序,删除限制只有 n n n 种可能的方案。前 n − 1 n - 1 n−1 种方案是取消 1 1 1 ~ n − 1 n-1 n−1 的某一个限制,然后空出来的所有时间贪心的分配到收益高的作物上。还有一种方案是取消收益最大的的作物的限制,然后空余的时间都分配给,最后 n n n 种方案去 max \max max 就是答案。枚举删除哪种作物的限制时,可以用两个树状数组维护剩余时间( r i − l i r_i - l_i ri−li) 和剩余时间花完的收益的后缀和,然后就可二分。总的时间复杂度为 O ( n log 2 n ) O(n\log^2{n}) O(nlog2n)。
// Author: Chuanhua Yu
// Time: 2024-11-03
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using vi = vector<int>;
const int N = 1e5 + 5;
struct BIT{
vector<ll> su;
int n;
BIT(int x) : n(x), su(x + 5, 0){}
inline void add(int pos, ll x){
while (pos){
su[pos] += x;
pos -= -pos & pos;
}
}
inline ll query(int pos){
ll res = 0;
while (pos <= n){
res += su[pos];
pos += -pos & pos;
}
return res;
}
};
struct nd{
ll w, l, r;
bool operator<(const nd &other) const{
return w < other.w;
}
} a[N];
ll m; int n;
int find(ll x, BIT &pid){
int l = 1, r = n + 1;
while(l < r){
int mid = (l + r) >> 1;
if(pid.query(mid) < x) r = mid;
else l = mid + 1;
}
return r;
}
void solve(){
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);
ll ans = 0, ti = m;
BIT pid(n), pval(n);
for(int i = n; i; i--){
ti -= a[i].l;
ans += a[i].l * a[i].w;
pid.add(i, a[i].r - a[i].l);
pval.add(i, (a[i].r - a[i].l) * a[i].w);
}
ll tmp = ans;
ans += ti * a[n].w;
for(int i = 1; i < n; i++){
pid.add(i, m - a[i].r + a[i].l);
ll res = tmp - a[i].l * a[i].w;
int nid = find(ti + a[i].l, pid);
res += pval.query(nid) + (ti + a[i].l - pid.query(nid)) * a[nid - 1].w;
ans = max(res, ans);
pid.add(i, a[i].r - m - a[i].l);
}
cout << ans << '\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
// int T;
// cin >> T;
// while(T --)
solve();
return 0;
}
Problem J. New Energy Vehicle(贪心)
题目大意:含 n n n 种电瓶的车,每种电瓶上界 a i a_i ai,耗 1 1 1 单位任意电瓶种的电力前进 1 1 1(只能向前),有 m m m 个充电站,每个充电可以 给一个指定的电瓶充电。求初始电瓶满的情况下最远可以行驶多远。
题目解析:队友写的待更新,先贴代码吧
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using vi = vector<int>;
const int N = 1e5 + 5;
const int inf=1e9;
struct node{
int idx,t;
bool operator<(const node& a)const{
if(idx!=a.idx)
return idx>a.idx;
else
return t>a.t;
}
};
int n,m;
ll a[N], b[N], x[N];
int t[N], nxt[N];
bool vis[N];
void solve(){
cin>>n>>m;
for(int i=1;i<=n;++i){
cin>>a[i];
b[i]=a[i];
vis[i]=0;
}
for(int i=1;i<=m;++i){
cin>>x[i]>>t[i];
nxt[i]=0;
vis[t[i]]=1;
}
map<int,int> mp;
for(int i=m;i>=1;--i){
int tmp=mp[t[i]];
nxt[i]=(tmp==0?inf:tmp);
mp[t[i]]=i;
}
priority_queue<node> q;
for(int i=1;i<=m;++i){
if(i==mp[t[i]]){
q.push({i, t[i]});
}
}
for(int i=1;i<=n;++i){
if(!vis[i]){
q.push({inf,i});
}
}
ll ans=0;
x[0]=0;
for(int i=1;i<=m;++i){
ll dis=x[i]-x[i-1];
while(!q.empty()&&dis){
node now=q.top();
ll tmp=min(dis,b[now.t]);
dis-=tmp;
b[now.t]-=tmp;
ans+=tmp;
if(b[now.t]==0)
q.pop();
}
if(dis!=0)
break;
while(!q.empty()){
node now=q.top();
if(now.idx<=i)
q.pop();
else
break;
}
b[t[i]]=a[t[i]];
q.push({nxt[i],t[i]});
}
while(!q.empty()){
node now=q.top();
q.pop();
ans+=b[now.t];
}
cout<<ans<<'\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int T;
cin >> T;
while(T --)
solve();
return 0;
}
Problem A. Build a Computer(位运算,图论,构造)
题目大意:给定 L L L, R R R,要构造一个 D A G DAG DAG,满足仅有一个起点和一个 终点。每条边有权值 0 / 1 0/1 0/1。从起点 d f s dfs dfs,把经过的 0 / 1 0/1 0/1 边权记录下来,每条到终点的路径会是一个二进制数(不含前导零、不重复),所有二进制数刚好是 [ L , R ] [L, R] [L,R] 内每个数的二进 制,要求 D A G DAG DAG 的节点数不超过 100 100 100,且每个节点的出度不超过 200 200 200, L ≤ R ≤ 1 0 6 L ≤ R ≤ 10^6 L≤R≤106。
题目解析:~~官方题解复读机(bushi。~~对于询问 [L, R] 我们可以把它拆成若干个子询问:
- 我们可以从 L L L 在二进制表示下的低位枚举到高位,遇到一个 0 0 0 时,考虑把 0 0 0 变成 1 1 1,同时右边所有数全部改为 0 0 0,得到一个新的数 L ′ L^′ L′;全部改为 1 1 1,得到一个新的数 R ′ R^′ R′。
- 若 R ′ ≤ R R^′ ≤ R R′≤R,则拆分出一个新的子询问 [ L ′ , R ′ ] [L^′ , R^′] [L′,R′],然后枚举下 一位;
- 否则的话,我们考虑开始从 R R R 在二进制表示下的高位枚举到低位(从第二位开始枚举),遇到一个 1 1 1 时,考虑把 1 1 1 变 成 0 0 0,同时右边所有数全部改为 0 0 0,得到一个新的数 L ′ L^′ L′;全部改为 1 1 1,得到一个新的数 R ′ R^′ R′。此时同样拆分出了一个新的 子询问 [ L ′ , R ′ ] [L^′ , R^′] [L′,R′],然后继续枚举下一位,直到枚举完所有位。
使用上述拆分方法,可以保证任意两个子询问的交为空,且所有子询问的并等于 [ L , R ] [L, R] [L,R]。并且对于每个子询问 [ L i , R i ] [L_i , R_i] [Li,Ri], 都满足 R i R_i Ri 与 L i L_i Li 去掉 L C P LCP LCP 后剩余的后缀分别是 11...1 11...1 11...1 和 00...0 00...0 00...0,这个可以直接通过连上满足第 i i i 个节点与第 i + 1 i + 1 i+1 个节点之间存在两条权值分别为 0 0 0 和 1 1 1 的边,最后一个节点连向终点的子图来进行构造。因此只需要 考虑对 L i L_i Li 和 R i R_i Ri 的 L C P LCP LCP,建字典树即可。
不难发现,建出来的字典树实际上和对 L L L 和 R R R 建字典树是类似的(对于每对 [ L i , R i ] [L_i , R_i] [Li,Ri], L i L_i Li 与 R i R_i Ri 的 L C P LCP LCP 一定是 L L L 或 R R R 的某个前缀后接 0 0 0 或 1 1 1)。字典树结点个数不超过 40 40 40,子图的节点个数不超过 20 20 20,因此满足总节点数不超过 100 100 100。
Problem L. A Game On Tree(树形结构,数学期望,组合数学)
题目大意:给定一棵 n n n 个点的树,从 n ( n − 1 ) 2 \frac{n(n−1)}{2} 2n(n−1) 条简单路径中等概率随机选择两条路径(选择的路径可以相同),记两条路径的公共边数量为 X X X,求 E ( X 2 ) E(X^2) E(X2),结果对 998244353 998244353 998244353 取模。
题目解析:看似是数学期望,其实期望是假的,考察的是计数能力。总共有 ( n ( n − 1 ) 2 ) 2 (\frac{n(n−1)}{2})^2 (2n(n−1))2 种选法,先计算所有选法的 X 2 X^2 X2 之和, 然后除以 ( n ( n − 1 ) 2 ) 2 (\frac{n(n−1)}{2})^2 (2n(n−1))2 即可。
具体来说,考虑两条路径的公共边为
e
1
,
e
2
,
.
.
.
,
e
k
e_1, e_2, ..., e_k
e1,e2,...,ek,那么对
X
2
X^2
X2 之和的贡献为(其中所有边长
∣
e
i
∣
|e_i|
∣ei∣ 均为
1
1
1):
(
∣
e
1
∣
+
∣
e
2
∣
+
.
.
.
+
∣
e
k
∣
)
2
=
∑
i
=
1
k
∣
e
i
∣
2
+
∑
1
≤
i
,
j
≤
k
∣
e
i
∣
⋅
∣
e
j
∣
(|e_1| + |e_2| + ... + |e_k |)^2 = \sum^k_{i=1}{|e_i|^2} + \sum_{1≤i,j≤k}{|e_i| · |e_j|}
(∣e1∣+∣e2∣+...+∣ek∣)2=i=1∑k∣ei∣2+1≤i,j≤k∑∣ei∣⋅∣ej∣
因此一种选法的贡献可以分成两种,一种由公共边
e
i
e_i
ei 产生,一种由公共边的边有序对
(
e
i
,
e
j
)
(e_i , e_j)
(ei,ej) 产生。我们不妨以
1
1
1 为根,令
s
i
z
[
u
]
siz[u]
siz[u] 表示以
u
u
u 为根的子树的大小,
s
u
m
[
u
]
=
∑
v
∈
s
u
b
t
r
e
e
u
(
s
i
z
[
v
]
2
)
sum[u] = \sum_{v\in subtree_u}{(siz[v]^2)}
sum[u]=∑v∈subtreeu(siz[v]2)。
-
对于第一种贡献,考虑每一条边 ( u , v ) (u, v) (u,v) 在多少种选法中会成 为公共边即可。不妨设 u u u 是 v v v 的父节点,则只需两条路径 的端点均分别在 ( u , v ) (u, v) (u,v) 两侧即可,故选法有 s i z [ v ] 2 ⋅ ( n − s i z [ v ] ) 2 siz[v]^2 · (n - siz[v])^2 siz[v]2⋅(n−siz[v])2。
-
对于第二种贡献,需要计算每个边有序对 ( e i , e j ) (e_i , e_j) (ei,ej) 在多少种选法中有贡献。考虑在 u u u 处仅计算两条边端点的最近公共祖先为 u u u 的边有序对的贡献。分两条边位于 u u u 的同一子树分支和不同子树分支两种情况分别讨论:
-
若两条边位于 u u u 的同一子树分支,由于仅考虑边端点的 L C A LCA LCA 为 u u u 的情况,因此存在一条边其中一个端点是 u u u , 另一个端点是 u u u 的某个儿子 v v v。另一条边则位于 v v v 的子树中,设 v ′ ∈ s o n v v'\in son_v v′∈sonv,两次选择路径均需要一个端点在子树 s u b t r e e v subtree_v subtreev 外,并且另一个端点得同时在 s u b t r e e v ′ subtree_{v'} subtreev′ 中,合法的选择总数有:
∑ v ∈ s o n u 2 × ( n − s i z [ v ] ) 2 ( s u m [ v ] − s i z [ v ] 2 ) \sum_{v\in son_u} {2 \times(n - siz[v])^2(sum[v] -siz[v]^2)} v∈sonu∑2×(n−siz[v])2(sum[v]−siz[v]2) -
若两条边位于 u u u 的不同子树分支,则选法很简单,每一条路径的两端点都得落在两子树中,所以合法的贡献总数有:
∑ v 1 , v 2 ∈ s o n u s u m [ v 1 ] ⋅ s u m [ v 2 ] \sum_{v_1,v_2\in son_u}{sum[v_1] · sum[v_2]} v1,v2∈sonu∑sum[v1]⋅sum[v2]
-
-
一次深度优先搜索即可实现上述统计。时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)。
ac代码参考:
// Author: Chuanhua Yu
// Time: 2024-11-05
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
using vi = vector<int>;
const int N = 1e5 + 5, mod = 998244353;
ll n, tot;
int head[N], ver[N<<1], nxt[N<<1];
ll siz[N], sum[N], ans;
ll inv(ll x){
ll res = 1, b = mod - 2;
while(b){
if(b & 1) res = res * x % mod;
b >>= 1;
x = x * x % mod;
}
return res;
}
inline void add(int x, int y){
ver[++tot] = y; nxt[tot] = head[x]; head[x] = tot;
}
void dfs(int x, int fa){
siz[x] = 1;
ll tsum = 0;
for(int i = head[x]; i; i = nxt[i]){
int y = ver[i];
if(y == fa) continue;
dfs(y, x);
siz[x] += siz[y];
sum[x] = (sum[x] + sum[y]) % mod;
tsum = (tsum + sum[y]) % mod;
}
sum[x] = (sum[x] + siz[x] * siz[x] % mod) % mod;
for(int i = head[x]; i; i = nxt[i]) {
int y = ver[i];
if (y == fa) continue;
// 公共边的贡献
ans = (ans + ((siz[y] * (n - siz[y]) % mod) * siz[y] % mod) * (n - siz[y])) % mod;
// 公共边有序对的贡献
ll tmp = 2ll * (n - siz[y]) * (n - siz[y]) % mod;
tmp *= (sum[y] - siz[y] * siz[y] % mod + mod) % mod;
ans = (ans + tmp % mod) % mod;
// 两点在不同子树且 LCA(u1, v1) = x = LCA(u2, v2) 对答案的贡献
ans = (ans + sum[y] * ((tsum - sum[y] + mod) % mod)) % mod;
}
}
void solve(){
tot = 1; ans = 0;
cin >> n;
memset(head, 0, (n + 1) * sizeof(int));
memset(siz, 0, (n + 1) * sizeof(ll));
memset(sum, 0, (n + 1) * sizeof(ll));
for(int i = 1; i < n; i++){
int x, y; cin >> x >> y;
add(x, y); add(y, x);
}
dfs(1, 0);
ll fenmu = 4ll * inv((n * n * (n - 1) % mod) * (n - 1) % mod) % mod;
ans = ans * fenmu % mod;
cout << ans << '\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int T;
cin >> T;
while(T --)
solve();
return 0;
}