链接:The 2023 Guangdong Provincial Collegiate Programming Contest
有中文题面,题意就省略了。
B Base Station Construction
思路
本题参考了官方题解。
注意观察题目数据 n , m n, m n,m 的和都不超过 5 e 5 5e5 5e5,那么我们dp就可以从这两方面考虑,这里我们从站点而不从区间来入手。
定义dp方程: f [ i ] : f[i]: f[i]: 前 i i i 个站点,且第 i i i 个站点必须建基站的最小花费。状态转移就是枚举上一个站点建在哪 j j j, f [ i ] = min j i − 1 f [ j ] + a [ i ] f[i] = \min_j^{i-1}f[j] +a[i] f[i]=minji−1f[j]+a[i].
注意一个问题,有些转移是不合法的,当存在区间 [ l , r ] [l, r] [l,r] 使得 l > j , r < i l > j,r<i l>j,r<i.这样就有区间中没有站点,这是不合法的。 考虑对于每个 i i i 找到 p [ i ] : p[i]: p[i]:最远的合法转移点,用优先队列求解,易知 p [ i ] p[i] p[i] 一定是单调递增的,我们可以将区间按右端点排序,对于所有右端点小于当前点 ( r < i ) (r < i) (r<i) 的区间存入优先队列大顶堆按左端点排序,这样每次从队首取元素判断当前最远点 k k k 是否合法,如果不合法 l > k l > k l>k,就将 k = l k = l k=l.
具体实现见代码:
struct seg{
int l, r;
bool operator <(const seg& A){
if(r == A.r) return l < A.l;
return r < A.r;
}
}s[N];
priority_queue<pair<int, int> > q;
sort(s + 1, s + 1 + m);
while(!q.empty()) q.pop();
for(int i = 1, j = 0, k = 0; i <= n; i ++){
while(j <= m && s[j].r < i) q.push({s[j].l, s[j].r}), j ++;
if(!q.empty() && q.top().first > k) k = q.top().first; // 队列中都是r < i 的,若是 l > k 说明两个站点之间没有包含到该区间,这是不合法的,之间让k跳到l
p[i] = k;
}
值得一提的是,官方题解中是用双指针求解 p [ i ] p[i] p[i] 的,时间更加优秀。
接下来的转移就很好想了,就是对于合法区间 f [ i ] = min j = p [ i ] i − 1 f [ j ] + a [ i ] f[i] = \min_{j=p[i]}^{i-1}f[j] + a[i] f[i]=minj=p[i]i−1f[j]+a[i]. 求区间最小值,可以用线段树维护,单点修改区间查询,转移时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。由于 p [ i ] p[i] p[i] 是单调递增的,可以用单调队列优化转移时间复杂度 O ( n ) O(n) O(n)。这里采用单调队列的方法。
最后的最后可以建立一个虚拟点来方便直接输出 a [ n + 1 ] = 0 a[n + 1] = 0 a[n+1]=0.
代码
/*
n和m之和都小于5e5 所以可以从这两方面来考虑,枚举站点和枚举区间
*/
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e5 + 10;
const ll inf = 1e18;
struct seg{
int l, r;
bool operator <(const seg& A){
if(r == A.r) return l < A.l;
return r < A.r;
}
}s[N];
int a[N], n, m;
ll f[N], p[N]; // f:前i个站点 且第i个站点必须建立电站的最小花费(枚举上一个站点j求解) p:要求两个站点之间不能有完整的区间,pi = 最小的满足有要求的站点j
priority_queue<pair<int, int> > q;
ll que[N];
void solve(){
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
a[++ n] = 0; // 虚拟站点
cin >> m;
for(int i = 1; i <= m; i ++){
cin >> s[i].l >> s[i].r;
}
sort(s + 1, s + 1 + m);
while(!q.empty()) q.pop();
for(int i = 1, j = 0, k = 0; i <= n; i ++){
while(j <= m && s[j].r < i) q.push({s[j].l, s[j].r}), j ++;
if(!q.empty() && q.top().first > k) k = q.top().first; // 队列中都是r < i 的,若是 l > k 说明两个站点之间没有包含到该区间,这是不合法的,之间让k跳到l
p[i] = k;
}
int tot = 0, top = 0;
f[0] = 0;
for(int i = 1; i <= n; i ++){ // 求的是合法区间中p[i] ~ i - 1的最小值,由于p[i]单调递增可以用单调队列维护
while(top <= tot && que[top] < p[i]) top ++;
f[i] = f[que[top]] + a[i];
while(tot >= top && f[que[tot]] > f[i]) tot --;
que[++ tot] = i;
}
for(int i = 1; i <= n; i ++) que[i] = 0;
cout << f[n] << "\n";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while(t --){
solve();
}
return 0;
}
D New Houses
思路
容易知道,对于不喜欢一起住的人即 a i < b i a_i<b_i ai<bi 的人,当迫不得已必须一起住的时候,价值就会减少 b i − a i b_i-a_i bi−ai,当出现这种情况的时候,肯定是选减少价值最少的去一起住,这是本题唯一的算法贪心点。
接下来我们就不用思考那么多边界问题,直接大力分类讨论!具体看代码,每行几乎都有详细注释。
代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6 + 10;
struct node{
int a, b;
};
bool cmp(node &A, node &B){
return A.b - A.a < B.b - B.a;
}
void solve(){
int n, m;
cin >> n >> m;
vector<node> A, B;
ll sumA = 0, sumB = 0;
for(int i = 1; i <= n; i ++){
int a, b;
cin >> a >> b;
if(a >= b) A.push_back({a, b}), sumA += a; // 按喜好区分,将价值求和
else B.push_back({a, b}), sumB += b;
}
int sizA = A.size(), sizB = B.size();
if(sizB == 0){ // 没有喜欢单独住的
if(sizA == 1) cout << A[0].b << "\n"; // 如果只有一个人,必须单独住
else cout << sumA << "\n"; // 直接输出全部一起住的价值
return ;
}
sort(B.begin(), B.end(), cmp); // 按如果不能独自住,按损失的最少的排序
if(sizA == 0){ // 全是喜欢独自住的
if(m < sizB * 2 - 1){ // 不能全部单独住
for(auto [a, b] : B){ // 枚举有多少是必须一起住的
sumB -= (b - a); // 将一起住的减少的值减去
m --;
sizB --;
if(m >= sizB * 2) break; // 满足剩下的人可以单独住结束
}
}
cout << sumB << "\n";
return ;
}
if(sizA == 1){ // 只有一个喜欢一起住的
ll ans = sumB;
m --;
if(m < sizB * 2){ // 剩下的人不能全部单独住
ans += A[0].a; // 喜欢一起住的人就能加上价值
for(auto [a, b] : B){
ans -= (b - a);
m --;
sizB --;
if(m >= sizB * 2) break;
}
}
else{ // 剩下的人可以全部单独住,分情况讨论
ans = max(sumB + A[0].b, sumB - (B[0].b - B[0].a) + A[0].a); // 全部单独住,或者有一对一起住
}
cout << ans << "\n";
return ;
}
// sizA > 1 && sizB > 0
// 喜欢一起住的肯定都一起住,剩下的位置让无法独自住,选减少价值最小的一起住
ll ans = sumA + sumB;
m -= sizA;
if(m < sizB * 2){
for(auto [a, b] : B){
ans -= (b - a);
m --;
sizB --;
if(m >= sizB * 2) break;
}
}
cout << ans << "\n";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while(t --){
solve();
}
return 0;
}
F Traveling in Cells
大致题意:每个格子有颜色和价值(两者都有修改操作)每次给定颜色集合 A A A 只能走集合内颜色的格子,问从 x x x 点开始最多能收集到多少价值。
思路
对于这种维护东西很多很杂无从下手的问题我们开始逐步简化。
对于每一个询问我们要解决的主要问题是:从给定点 x x x 出发最远能走到的边界 [ l , r ] [l, r] [l,r] 是多少,解决这个问题后,只需要用一个线段树/树状数组就可以维护单点修改,区间求和输出答案了。
对于从 x x x 开始最多能走多远,无疑是有单调性的,即如果往左边走我们不能走到 l l l 点,那么 1 ∼ l − 1 1\sim l-1 1∼l−1 肯定也不能走到,右边同理。既然有单调性我们就考虑二分。要二分就必须知道一个区间内颜色有多少个是属于集合 A A A 的,注意每次给定颜色集合大小 k k k,保证 ∑ k i ≤ 1 e 6 \sum k_i \leq 1e^6 ∑ki≤1e6,所以我们可以对每个颜色分开查询,最后算总数是否等于我们二分出来的该区间长度即可。
到此为此问题被简化为询问一个区间内指定单一颜色的个数(带修),我们继续简化问题。如果问题保证所有点只有两种颜色我们是否能解决该问题?显然用线段树就能很方便解决,将颜色分为黑白
0
/
1
0/1
0/1 维护每个区间的价值,区间之和就代表白色点的个数。
回到有多种颜色的问题,我们是否能对每一种颜色都建一棵线段树呢?对于颜色
i
i
i 的线段树,颜色
i
i
i 代表
1
1
1,其他颜色代表
0
0
0. 关于
n
n
n 棵线段树空间不够的问题我们可以动态开点,每次只新增一条链空间复杂度为
O
(
(
n
+
q
)
l
o
g
n
)
O((n + q)logn)
O((n+q)logn).文章来源:https://www.toymoban.com/news/detail-741092.html
时间复杂度:二分答案 + 线段树,上界取决于询问的集合总大小
k
k
k,所以为
O
(
(
l
o
g
n
)
2
∑
k
i
)
O((logn)^2 \sum k_i)
O((logn)2∑ki).
到此为止问题已经在逐步简化下解决了具体见代码及注释。文章来源地址https://www.toymoban.com/news/detail-741092.html
代码
/*
对区间的价值用树状数组维护
对颜色用线段树维护,即对每种颜色建立一棵线段树,动态开点保证空间,对于颜色i, 区间[l, r].sum 表示的是区间中颜色i的数量
*/
#include <bits/stdc++.h>
using namespace std;
#define ll long long
typedef vector<int> Vec;
const int N = 1e5 + 10, MAX = 1e5;
int n, q;
int v[N], c[N];
int T[N], tot; // T[i]:颜色为i的线段树的根
struct node{
int ls, rs, sum;
}tr[N * 200];
void update(int& p, int loc, int k, int l = 1, int r = MAX){ // 对于该颜色更新点loc的价值+1
if(!p) p = ++ tot;
tr[p].sum += k;
if(l == r) return ;
int mid = (l + r) >> 1;
if(loc <= mid) update(tr[p].ls, loc, k, l, mid);
else update(tr[p].rs, loc, k, mid + 1, r);
}
int query(int p, int ql, int qr, int l = 1, int r = MAX){ // 对于颜色i查询区间[ql, qr]该颜色的个数
if(!p) return 0;
if(ql <= l && r <= qr) return tr[p].sum;
int mid = (l + r) >> 1, ans = 0;
if(ql <= mid) ans += query(tr[p].ls, ql, qr, l, mid);
if(qr > mid) ans += query(tr[p].rs, ql, qr, mid + 1, r);
return ans;
}
ll val[N]; // 树状数组维护单点修改和区间求和
int lowbit(int x){ return x & -x; }
void update(int r, int k){
for(int i = r; i <= n; i += lowbit(i)) val[i] += k;
}
ll get_sum(int l, int r){
ll ans = 0;
for(int i = r; i; i -= lowbit(i)) ans += val[i];
for(int i = l - 1; i; i -= lowbit(i)) ans -= val[i];
return ans;
}
Vec ci; // 每次查询的颜色集合
bool check(int len, int l, int r){
int sum = 0;
for(auto x : ci){
sum += query(T[x], l, r); // 对于所有集合中的颜色查询[l, r].sum
}
return sum == len;
}
void get_ans(){
int x, k;
cin >> x >> k;
ci.clear();
for(int i = 1, y; i <= k; i ++){
cin >> y;
ci.push_back(y);
}
sort(ci.begin(), ci.end());
ci.erase(unique(ci.begin(), ci.end()), ci.end()); // 去重
int l = 1, r = n;
int Lr = x;
while(l <= Lr){ // 二分找最远能到的左边界
int mid = (l + Lr) >> 1;
if(check(x - mid + 1, mid, x)) Lr = mid - 1;
else l = mid + 1;
}
int Rl = x;
while(Rl <= r){ // 二分找最远合法的右边界
int mid = (Rl + r) >> 1;
if(check(mid - x + 1, x, mid)) Rl = mid + 1;
else r = mid - 1;
}
cout << get_sum(l, r) << "\n"; // 输出求和
}
void clear(){
for(int i = 0; i <= tot; i ++) tr[i] = {0, 0, 0};
for(int i = 1; i <= n; i ++) T[i] = val[i] = 0;
tot = 0;
}
void solve(){
cin >> n >> q;
clear();
for(int i = 1; i <= n; i ++){
cin >> c[i];
update(T[c[i]], i, 1);
}
for(int i = 1; i <= n; i ++){
cin >> v[i];
update(i, v[i]);
}
for(int i = 1; i <= q; i ++){
int op, p, x;
cin >> op;
if(op == 1){ // 更改颜色
cin >> p >> x;
update(T[c[p]], p, -1);
c[p] = x;
update(T[c[p]], p, 1);
}
else if(op == 2){ // 更改价值
cin >> p >> x;
update(p, x - v[p]);
v[p] = x;
}
else get_ans();
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t;
cin >> t;
while(t --){
solve();
}
return 0;
}
到了这里,关于2023 CPC 广东省赛(B,D)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!