1、st算法与RMQ问题
RMQ问题介绍
st算法是基于倍增原理的算法,是解决RMQ问题的一个非常优秀的算法。那么我们先来介绍一下什么是RMQ问题 。RMQ就是给一个静态的长度为n的序列,我们进行m次查询,每次查询区间l到r的最大值与最小值。这个问题的最朴素的解决方法就是用for循环来遍历:
以查询最大值为例
int ma = -1;
for(int i =l;i<=r;++i)
if(a[i] > ma)swap(a[i] ,ma}
最后就可以得到ma,那么它的时间复杂度是多少呢?比较的复杂度是O(n),m次查询,总的时间复杂度是O(mn),效率很低。有没有一种方法可以极大的提高效率呢?答案是有的,那就是st算法
st算法
st算法源于这样一个原理:如果把大区间分成两个小区间,那么大区间的最值就等于两个小区间的最值。我们可以使用倍增的思想来划定小区间,这样可以把时间复杂度降低到O(nlogn)。
如何实现呢?我们需要定义一个st数组 , st[I][j]表示左端点为i,区间长度为2的j次方的区间的最值,也就是从i到i + 2的j次方 -1,这样一个左闭右闭的区间。
dp[i][j] = max(dp[i][j-1] , dp[i+(1<<(j-1))][j-1]);
//以寻找最大值为例
//1<<(j-1)是1左移j-1位 ,就是2的j-1次方
这个代码就是dp数组的预处理。
那么如何查询呢?
区间长度len是r-l+1。我们把区间分成两个x , 令x是比len小的2的最大倍数,2x同时也要>=len,
根据dp数组的定义 ,2的k次方 =x。那么已经知道len,如何求k?k就是log以2为底len的对数向下取整。
int k =log(r-l+1) / log(2.0);
最后再查询
max(dp[l][k] , [r - 1<<k +1][k]);
我们可以来做一道例题:1.区间最大值 - 蓝桥云课 (lanqiao.cn)
ac代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int n,m;
int a[N] , st[N][30];
int getmax(int l ,int r){
int k = log(r -l +1) / log(2.0);
return max(st[l][k] , st[r - (1<<k)+1][k]);
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;++i)cin>>a[i];
//现在对st数组进行初始化和预处理
for(int i =1;i<=n;++i)st[i][0] = a[i]; //初始化
//接下来是预处理
for(int j = 1;j<=30;++j){
for(int i =1;i<=n;++i){
if(i +(1 <<j) - 1 <=n) // 这个if是防止越界{
st[i][j] = max(st[i][j-1] , st[i + (1 << (j-1))][j-1]);
}
}
while(m--){
int l,r;cin>>l>>r;
cout<<getmax(l ,r)<<'\n';
}
return 0;
}
2、一点点并查集
并查集是什么?并查集主要运用在处理一些不相交的集合的合并,查询等问题,经典的应用有连通图 ,最小生成树,最近公共祖先等。
这里先介绍一点基础并查集(因为我只学了一点基础并查集嘿嘿), 初始时,每个元素的根都指向自己,如果想要合并某两个元素,就把其中一个元素的父亲指向另一个元素的根,如果又出现一个元素想要合并呢,就再把它的父亲指向那个元素的根。
我们可以先写一个找每个元素的根的函数
int root(int x){
return pre[x] = x?x : root(pre[x]);
}
//pre[i]是先定义的一个父亲数组,表示i的父亲
如果要合并呢,就写一个合并函数
void merge(int x ,int y){ //把x和y合并
x = root(x) ,y = root(y);
if(x == y)return; //如果x和y的根一样,说明他们已经合并了,直接结束
pre[x] =y; //否则的话 ,x的父亲指向y的根
}
那么其实我们第一段代码的时间复杂度是非常高的,我们可以用路径压缩来优化我们的代码
int root (int x){
return pre[x] = pre[x] == x ? x : root(pre[x]);
}
我们可以用一道例题来理解并查集:P3367 【模板】并查集 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)a
ac代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 8;
int pre[N];
int root(int x){
return pre[x] = pre[x] ==x ? x : root(pre[x]); //用压缩路径来进行优化
}
void merge(int x , int y){
x =root(x) , y = root(y);
if(x == y)return;
pre[x] = y;
}
int main(){
int n,m;cin>>n>>m;
for(int i =1;i<=n;++i)pre[i] = i;
while(m--){
int op , x, y;
cin>>op>>x>>y;
if(op == 1)merge(x , y); //合并x和y
else cout<<(root(x) == root(y) ? "Y":"N")<<'\n'; //查询
}
return 0;
}
3、洛谷 精卫填海
传送门:P1510 精卫填海 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题乍一看,这不是01背包吗,这种题还是黄题?难道不应该是红题(入门)吗?
再仔细一看,实则大有玄机(但其实也是比较水的,评论区说的。。)。我们先来理解一下题意,说我们需要v体积的石头来填满海 , 但是鸟只有c的体力 ,有n个石头,每个石头有对应的体积和搬走它所需的体力值,问如果精卫能把东海填平,则输出她把东海填平后剩下的最大的体力,否则输出 Impossible
(不带引号)。
那么我们还是先用01背包,来找到我们耗费完所有的体积最多能搬走多少体积的石头
cin>>v>>n>>c;
for(int i =1;i<=n;++i){
int sv , sc;
cin>>sv>>sc;
for(int j =c;j>=sc;--j){
dp[j] = max(dp[j] , dp[j-sc]+sv);
}
}
此时我们算出dp[c]是消耗了c体力最多能搬走的石头体积,那么我们和v做比较,如果比它小,就说明不能填满
if(dp[c] <v){
cout<<"Impossible";
return 0;
}
关键的来了,如果能填满的话,我们要找耗费最小体力,就从1开始枚举,枚举到能填满的那个体力,就是最小体力。我们要用一个量来维护这个体力,比如我用的是ans
for(int i =1;i<=c;++i)
{
if(dp[c] >= v){
ans = i;
break;
}
}
那么到这里就做完了,请看ac代码
//我们还需要v的石头 , 还有n个石头 ,每块的体积不同k ,需要的体力为m 还有c的体力
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 +9;
int dp[N];
int v,n,c;
int main(){
cin>>v>>n>>c;
for(int i =1;i<=n;++i){
int sv , sc;
cin>>sv>>sc;
for(int j =c;j>=sc;--j){
dp[j] = max(dp[j] , dp[j-sc]+sv);
}
}
if(dp[c] <v){
cout<<"Impossible";
return 0;
}
int ans =0;
for(int i =1;i<=c;++i){
if(dp[i] >= v){
ans =i;
break;
}
}
cout<<c -ans;
return 0;
}