第十届CCPC哈尔滨站(CCPC 2024 Harbin Site)8题题解

第十届中国大学生程序设计竞赛 哈尔滨站(CCPC 2024 Harbin Site)

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 1n109

题目解析: 队友写的,题本身也算个签到,先贴个代码。

// 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 n105

题目解析

  • 求凹包的最大面积,可以理解为求凸包后,再增加一个点后的图形的面积
  • 考虑把原凸包的点删掉后剩下的点怎么增加才能让面积最大
  • 对于凸包的每一条边,距离边最近的点是最优的
  • 而这个点,一定在剩下的点的凸包上
  • 因此对剩下的点再求一遍凸包
  • 两个图形间,外边和内点的最小距离,可以用双指针维护
#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 n1 种方案是取消 1 1 1 ~ n − 1 n-1 n1 的某一个限制,然后空出来的所有时间贪心的分配到收益高的作物上。还有一种方案是取消收益最大的的作物的限制,然后空余的时间都分配给,最后 n n n 种方案去 max ⁡ \max max 就是答案。枚举删除哪种作物的限制时,可以用两个树状数组维护剩余时间( r i − l i r_i - l_i rili) 和剩余时间花完的收益的后缀和,然后就可二分。总的时间复杂度为 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 LR106

题目解析:~~官方题解复读机(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 RR,则拆分出一个新的子询问 [ 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(n1) 条简单路径中等概率随机选择两条路径(选择的路径可以相同),记两条路径的公共边数量为 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(n1))2 种选法,先计算所有选法的 X 2 X^2 X2 之和, 然后除以 ( n ( n − 1 ) 2 ) 2 (\frac{n(n−1)}{2})^2 (2n(n1))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=1kei2+1i,jkeiej
因此一种选法的贡献可以分成两种,一种由公共边 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]=vsubtreeu(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(nsiz[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 vsonv,两次选择路径均需要一个端点在子树 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)} vsonu2×(nsiz[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,v2sonusum[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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Chuanhua‘blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值