《算法竞赛进阶指南》读书笔记汇总
这里面是我在阅读《算法竞赛进阶指南》这本书时的一些思考,有兴趣可以瞧瞧!
如若发现什么问题,可以通过评论或者私信作者提出。希望各位大佬不吝赐教!
许多问题都可以使用二叉堆来进行优化,下面直接看例题吧。
【例题】超市(AcWing145)
题目链接
思路: 容易想到一个贪心策略:对于每个时间
t
t
t,在保证不卖出过期商品的情况下,尽量卖出利润前
t
t
t大的商品。具体做法如下:
1.我们将所有商品按照过期时间从小到大排序,建立一个初始为空的小根堆,然后依次扫描每一个商品。
2.对于当前商品,如果当前过期时间等于堆中商品数目,说明前
t
t
t天已经安排了
t
t
t个未过期商品准备卖出,那么如果堆顶商品利润小于当前商品利润,则用当前商品替换掉堆顶。
3.若当前商品的过期时间大于堆中商品数目,直接把该商品插入堆中。
4.最终,堆中的商品就是我们所要出售的商品啦。
可以体会一下维护堆的过程。
AC代码:
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N = 10005;
int n;
PII goods[N];
void solve(){
for(int i = 1;i <= n;i ++)
{
int p,d;
scanf("%d%d",&p,&d);
goods[i] = {d,p};
}
sort(goods + 1,goods + 1 + n);
priority_queue<int,vector<int>, greater<int> > q;
for(int i = 1;i <= n;i ++){
if(q.size() < goods[i].first) q.push(goods[i].second);
else if(q.top() < goods[i].second) q.pop(),q.push(goods[i].second);
}
int ans = 0;
while(q.size()){
ans += q.top();
q.pop();
}
printf("%d\n",ans);
}
int main(){
while(~scanf("%d",&n))
solve();
return 0;
}
【例题】序列(AcWing146)
题目链接
思路:我们考虑这样一个简化问题,即
M
=
2
M=2
M=2时,问题变为从两个长度为
N
N
N的序列中找出
N
N
N对和最小的数。
假设两个序列分别为
A
A
A与
B
B
B,我们先把
A
A
A序列从小到大排序,然后对于所有的数对(共有
N
2
N^{2}
N2个),考虑一个分组的思想(来自y总)。
分别为:
B
[
1
]
+
A
[
1
]
,
B
[
1
]
+
A
[
2
]
,
.
.
.
,
B
[
1
]
+
A
[
N
]
B[1] + A[1],B[1]+A[2],...,B[1]+A[N]
B[1]+A[1],B[1]+A[2],...,B[1]+A[N]
B
[
2
]
+
A
[
1
]
,
B
[
2
]
+
A
[
2
]
,
.
.
.
,
B
[
2
]
+
A
[
N
]
B[2]+A[1],B[2]+A[2],...,B[2]+A[N]
B[2]+A[1],B[2]+A[2],...,B[2]+A[N]
.
.
.
.
.
.
......
......
B
[
N
]
+
A
[
1
]
,
B
[
N
]
+
A
[
2
]
,
.
.
.
,
B
[
N
]
+
A
[
N
]
B[N]+A[1],B[N]+A[2],...,B[N]+A[N]
B[N]+A[1],B[N]+A[2],...,B[N]+A[N]
我们发现,每一组数对里,数对和都是单调不降的,那么我们可以利用一个小根堆维护两个指针和一个数对和,也就是一个三元组
i
,
j
,
s
u
m
{i,j,sum}
i,j,sum,
i
,
j
i,j
i,j分别表示指向
B
B
B数组与
A
A
A数组的指针。初始把每一组的第一个数对放进小根堆中,每次取出堆顶元素(当前的最小数),把它的
j
j
j指针加一,将得到的新数对放进小根堆中。重复取出
N
N
N即为前
N
N
N小的数对和。
那么如果要把
M
M
M个序列合并,那么我们可以从第一个序列开始一个一个向下合并,不难证明这个做法的正确性。
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 2005;
int n,m;
int a[N][N];
int tmp[N];
struct node{
int i,j,sum;
bool operator < (const node& b)const{
return sum > b.sum;
}
};
void solve(){
cin >> m >> n;
for(int i = 1;i <= m;i ++)
for(int j = 1;j <= n;j ++)
cin >> a[i][j];
sort(a[1] + 1,a[1] + 1 + n);
for(int i = 2;i <= m;i ++){
priority_queue<node> q;
for(int j = 1;j <= n;j ++){
q.push(node{j,1,a[i][j] + a[i - 1][1]});
}
for(int j = 1;j <= n;j ++){
node cur = q.top();
q.pop();
tmp[j] = cur.sum;
q.push(node{cur.i,cur.j + 1,a[i][cur.i] + a[i - 1][cur.j + 1]});
}
memcpy(a[i],tmp,sizeof(tmp));
}
for(int i = 1;i <= n;i ++){
if(i != 1) printf(" ");
printf("%d",a[m][i]);
}
puts("");
}
int main(){
int t;cin >> t;
while(t --)
solve();
return 0;
}
下面再看看一道习题。
【练习】黑盒子(AcWing162)
题目链接
思路:我们可以使用一个大根堆和一个小根堆来维护,大根堆维护排名前
i
−
1
i-1
i−1个数,小根堆维护排名在
i
i
i以及之后的数。对于
G
E
T
GET
GET操作,我们只需要把小根堆堆顶输出,再把小根堆堆顶放到大根堆中。对于
A
D
D
ADD
ADD操作,如果当前数大于或等于小根堆堆顶(即
A
[
i
]
A[i]
A[i]),则把当前数放到大根堆中;如果如果当前数小于大根堆堆顶,那么把当前数放到小根堆中,再把小根堆堆顶放到大根堆中。这题思想就是一个对顶堆,与动态维护中位数那题类似。
AC代码:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 30005;
int n,m;
LL a[N];
int u[N];
void solve(){
cin >> n >> m;
for(int i = 1;i <= n;i ++)
cin >> a[i];
for(int i = 1;i <= m;i ++)
cin >> u[i];
priority_queue<LL, vector<LL>, greater<LL> > q1; // 小顶
priority_queue<LL> q2; // 大顶
int i = 0, k = 1;
for(int j = 1;j <= n;j ++){
if(q1.empty() || a[j] >= q1.top()) q1.push(a[j]);
else{
q2.push(a[j]);
q1.push(q2.top());
q2.pop();
}
while(j == u[k]){
i ++;
q2.push(q1.top());
cout << q1.top() << endl;
q1.pop();
k ++;
}
}
}
int main(){
solve();
return 0;
}