济南 CSP-J 刷题营 Day4 数据结构

Solution

T1 出现次数

原题链接

4102: 出现次数

简要思路

利用类似前缀和的 “后缀和” 来记录下每个数后面有几个未重复出现的数,定义一个 \(f\) 数组来判断是不是第一次出现(实现去重)。

完整代码

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int MAXN=1e5+5;

int n,q;
int f[MAXN];//判断是否是第一次出现
int hzh[MAXN];//后缀和
int a[MAXN];

signed main(){
	cin>>n>>q;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	for(int i=n;i>=1;i--){//倒序遍历
		if(f[a[i]]==0){//如果是第一次出现
			hzh[i]=hzh[i+1]+1;
			f[a[i]]=1;//标记已经出现
		}
		else{
			hzh[i]=hzh[i+1];
		}
	}
	
	while(q--){
		int x;
		cin>>x;
		cout<<hzh[x]<<endl;//直接输出就行
	}
	
	return 0;
}

T2 组模拟赛

原题链接

4103: 组模拟赛

简要思路

设当前已经举办了 \(ans\) 场模拟赛,每次枚举 \(m\) 个难度的题,如果每道题的数量都大于 \(ans\),那么就再让答案 ans++

完整代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

int n,m;
int a[100005];
int f[100005];//记录每种难度的比赛出现几次
int num[100005];//num[i] 代表第 i 场比赛的举办时间

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	if(n==m){//特判
		cout<<n<<endl<<n<<endl;
		return 0;
	}
	
	int ans=0;
	for(int i=1;i<=n;i++){
		f[a[i]]++;//难度为 a[i] 的题的数量 +1
		if(i<m)continue;//如果数量过少,不可能组成一场模拟赛
		else{
			bool yes=1;
			for(int j=1;j<=m;j++)
				if(f[j]<=ans){//如果不够数量,不能够组成
					yes=0;
					break;
				}
			if(yes==1)num[++ans]=i;//记录
		}
	}
	
	cout<<ans<<endl;
	for(int i=1;i<=ans;i++)
		cout<<num[i]<<' ';
	return 0;
}

T3 完成作业

原题链接

4104: 完成作业

简要思路

第一种做法

对数组按照时间进行倒序排序,如果时间相同就取分数最大的,如果不选就把时间往前推。整个过程用优先队列)来实现。

第二种做法

对数组按照时间进行正序排序,每个时间点先不用管后面的,先选当前的最大值,如果后面有比之前更优的选择就更改替换之前的操作,同样要哪来实现这个反悔贪心

完整代码

#include<bits/stdc++.h>

using namespace std;
const int MAXN=1e5+5;

pair<int,int> a[MAXN];
priority_queue<int,vector<int>,greater<int>> q;//优先队列(堆)
int n
int now,last,ans;

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i].first>>a[i],second;
	sort(a+1,a+n+1);//按照时间进行正序排序
	
	for(int i=1;i<=n;i++){//反悔贪心
		now+=a[i].first-last;
		last=a[i].first;
		if(now>0){
			now--;
			ans+=a[i].second;
			q.push(a[i].second);
		}
		else if(q.top()<a[i].second){
			ans-=q.top();
			ans+=a[i].second;
			q.pop();
			q,push(a[i].second);
		}
	}
	cout<<ans<<endl;
	return 0;
}

T4 涂刷油漆

原题链接

4105: 涂刷油漆

简要思路

处理出每个操作来看能刷到什么高度(也就是 \(k\) 个数的最小值),ST 表或单调队列。
看一个柱子的最大高度是经过该柱子的所有区间的最大值。
用贪心,在保证 \(1~i\) 已经到达了最大高度的前提下,让后面的柱子经可能得高。
如果未到达最大高度,我们就要进行多一次操作,但是要保证对后边的柱子影响越多。
在高度最大的条件下位置最右的区间。
记录延伸到了哪,高度是多少。
\(a_{i+1}=h,i+1<p\),如果不满足就向后遍历,如果不符合就让答案加一并增加一个区间

完整代码

代码挂了,在调,回来补。QAQ__
施工中ing~~~

讲课笔记

前缀和和差分

提前记录下,以防 TLE,这样可以实现 O(1) 的查询输出。

  • 一维前缀和与差分

  • 多维前缀和与差分
    拆分开进行计算。

  • 树上前缀和与差分

  • 高维前缀和(欧氏空间上)

  • 高维前缀和(偏序集的直积空间上)

Hash 表

把字符串通过某一种方法替换为一个数字来进行比较两个字符串是否相等。

  • 字符串 Hash
    单模哈希、双模哈希、自然溢出。

  • 探查法/拉链法

链表、栈、队列

比较简单,范 sir 讲过。
这里要注意一个坑:双端队列的 deque 空间特别大,能不用就别用,容易挂分。
简单提一下这三种数据结构的用处(可是都不会)。

  • 链表
  1. 快速插入、删除

  2. 区间覆盖问题

  1. 表达式树

  2. 单调栈

  3. 笛卡尔树

  4. 离线 RMQ

  • 队列
  1. 队列和双端队列

  2. 单调队列

  3. 优先队列(一般为二叉堆,俗称 堆)

  4. 优化 DP

倍增、并查集

zhx 巨佬讲过这两种算法,简单带一下。

倍增

给定 \(T\) 次询问,每次给定 \(l,r\),求该区间内的最大值。

  • 核心思想:
    \(f_{i,j}\) 代表从 \(a_i\) 开始的 \(2^j\) 个数的最大值。

  • 小技巧:
    \(n\) 个数的什么东西,区间太长,一般会在中间“砍一刀”,最后只保留一个数。

  • 算法(处理 \(f_{i,j}\)):
    \(f_{i,0}=a_i\)
    \(f_{i,j}=max(f_{i,j-1},f_{i+2^{j-1},j-1})\)

即:
f[i][0]=a[i]
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1])

  • 做法(用 \(f_{i,j}\) 来算答案):
    性质:一个数被算多次在求最大值中也不会影响结果。

具体做法:一个区间长为 \(len\),寻找两个为 \(2\) 的整数倍的数,使得这两个数相加大于 \(len\),让 \(j\) 的值分别等于这两个数。

#include<bits/stdc++.h>
using namespace std;

//n<=100000 
//所以f的第二个维度只需要保证2^j>=n即可 
int n,a[100005],f[100005][20];
int x[100005]; 
//x[i]代表长度为i的区间 用两个长度为2^x[i]的区间能够覆盖 
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	x[1]=0;
	for(int i=2;i<=n;i++)
		x[i]=x[i>>1]+1;//算j 
		//x[111]=x[55]+1 
		
	for(int i=1;i<=n;i++)
		f[i][0]=a[i];
		
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);//预处理 
	
	int T;
	cin>>T;
	while(T--){
		int l,r;
		cin>>l>>r;
		int len=r-l+1;
		cout<<max(f[l][x[len]],f[r-(1<<x[len])+1][x[len]])<<endl;//算结果 
	}
}

可以用来实现 LCA

并查集

\(n\) 个点,每个点都有一条指向自己的边。
添加 \(i\)\(j\) 的边,就把 \(i\) 指向自己的那条边掰开,指向 \(j\)
如果 \(i\) 上指向自己的边已经掰开,我们就把与 \(i\) 联通的点的指向自己的边掰开,指向 \(j\)

int go(int p){//看一下点 p 沿着并查集箭头最后会走到哪里 
	if(to[p]==p)return p;//指向自己
	else return go(to[p]);//递归调用 
} 

可以用来实现 Kruscal

如果两个点在并查集中可以不断走向同一个点,那么这两个点就属于同一个连通块。

题单

题目 是否通过
P1969 [NOIP2013 提高组] 积木大赛 \(\color{yellowgreen}{√}\)
[AGC005B] Minimum Sum \(\color{red}{×}\)
P1540 [NOIP2010 提高组] 机器翻译 \(\color{yellowgreen}{√}\)
P1090 [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G \(\color{yellowgreen}{√}\)
P3957 [NOIP2017 普及组] 跳房子 \(\color{red}{×}\)
P1525 [NOIP2010 提高组] 关押罪犯 \(\color{red}{×}\)
P1196 [NOI2002] 银河英雄传说 \(\color{red}{×}\)
P1198 [JSOI2008] 最大数 \(\color{red}{×}\)
P3295 [SCOI2016] 萌萌哒 \(\color{red}{×}\)
P2058 [NOIP2016 普及组] 海港 \(\color{red}{×}\)

今日总结

  • 有不会的问题抓紧时间问老师,争取当天弄懂。

  • 赛时不要怕,直接暴力,可以分讨暴力骗分。

posted @ 2023-07-27 20:38  CheZiHe929  阅读(85)  评论(0编辑  收藏  举报