链接:The 2023 Guangdong Provincial Collegiate Programming Contest
有中文题面,题意就省略了。
本题参考了官方题解。
注意观察题目数据 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,rl>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
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;
}
容易知道,对于不喜欢一起住的人即 a i < b i a_i
接下来我们就不用思考那么多边界问题,直接大力分类讨论!具体看代码,每行几乎都有详细注释。
#include
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;
}