补题日记(4)

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;
}

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值