文章目录
The Fourth Week
会当凌绝顶,一览众山小。 ————杜甫
一、前言
周二蓝桥杯训练,oi赛制盲打了简直,280分,B题不知道怎么个事大家过的乱七八糟反正我没过。
周四,做了2023天梯赛,做了俩个半小时125分,不过再多做半个小时也未必能做出来就是了,L1做了一个半小时,L2用了大半个小时做了一道题,但是感觉好歹是能做出来的了,没有排行榜对比但比平时训练好一点点,继续加油。
周五蓝桥杯训练,185分打的什么玩意,第一第二题都没过。这赛制也太可怕了吧,改一点点就全对全错了。继续补题中。
二、算法
1.快速排序
俩道典型的快排。
随机寻找一个key,用low和high从左右遍历,可以先从左边遍历,找到第一个大于等于key的i后转到右边,找到第一个小于等于key的值j,a[i] > a[j],如果i小于j的话,得交换这俩个数字。
关于时间复杂度的问题,由于key的选取的不同,在最优情况下,每次都分成均匀的俩半,也就是一个二叉树,为O(nlogn);最坏的情况是为正逆序排序,一颗斜树,时间复杂度为O(n2)。
关于空间复杂度的问题,空间复杂度主要是栈造成的,平均情况是O(logn),一般不会MLE(除非写错了)。
另外提一下STL中的sort函数,其实会比快排好用感觉,会自动选取最优的排序方式,包括快排,插入排序和堆排序,这个快排算法写在这主要是为了学习一下,实用性好像不是很高。sort的头文件就是algorithm。
<1>(AcWing 785)
快速排序
题解:
快速排序的模版,给一个长度为n的数列进行排序。
见注释。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
long long a[100005];
void quick_sort(int low,int high){
if(low >= high)return ;
//排序完毕返回
int temp=a[(low + high)/2];
//随便找个key
int i=low-1;
int j=high+1;
while(i<j){
while(a[++i]<temp);
//i一直找到从左向右第一个大于等于temp的值
while(a[--j]>temp);
if(i<j){
//对i和j位置上的数据进行比较
swap(a[i],a[j]);
}
}
quick_sort(low,j);
//继续排序
quick_sort(j+1,high);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
//关闭输入输出流,降低时间复杂度
int n;
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
quick_sort(0,n-1);
//一个快排
for(int i=0;i<n;i++){
cout<<a[i]<<' ';
}
return 0;
}
<2>(AcWing 786)
第k个数
又一个模版
题解:
输入n,k,寻找第k小的数字。
见代码。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
long long a[100005];
int n,k;
void quick_sort(int low,int high){
if(low>=high){
return ;
}
int temp=a[(low+high)>>1];
int i=low-1;
int j=high+1;
while(i<j){
while(a[++i]<temp);
while(a[--j]>temp);
if(i<j){
swap(a[i],a[j]);
}
}
if(k-1 > j){
quick_sort(j+1,high);
}
else if(k-1<=j){
quick_sort(low,j);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>k;
for(int i=0;i<n;i++){
cin>>a[i];
}
quick_sort(0,n-1);
cout<<a[k-1]<<endl;
return 0;
}
2.dfs
<1>(洛谷P9241)
【蓝桥杯 2023省B】飞机降落
没做出来不想说了。
题解:
T组数据N行t,d,l,分别代表这n架飞机的可以降落时间是t到t+d,降落时长为L。
题意较为简单,数据范围不大,可以用dfs直接暴力做出,或者动态规划可以处理大数据情况。见注释。
代码:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int n;
bool st[15];
//标记飞机是否降落
struct plane {
int t;
int d;
int l;
} f[15];
//定义一个飞机结构体
bool dfs(int deepth, int time) {
if (deepth == n) {
return true;
}
//所有飞机降落即正确
for (int j = 1; j <= n; j++) {
if (st[j] == 0 && time <= f[j].t + f[j].d) {
//飞机未降落且可以降落的话
st[j] = true;
if (dfs(deepth + 1, max(time, f[j].t) + f[j].l)) {
return true;
}
st[j] = false;
}
}
return false;
}
//一个dfs深搜
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t;
cin >> t;
while (t--) {
memset(st, 0, 15);
//每次飞机降落情况全部赋值为0;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> f[i].t >> f[i].d >> f[i].l;
}
if (dfs(0, 0)) {
cout << "YES" << endl;
}
//可以全部降落
else {
cout << "NO" << endl;
}
}
return 0;
}
<2>(洛谷P8662)
【蓝桥杯 2018省AB】全球变暖
题解:
题目给出一张海域照片也就是a[n][n],上下左右四个相邻像素中有海洋的都会被淹没。求最后有多少岛屿会被淹没。
具体见注释,写几个主要问题。
1.算了没有被完全淹没的岛屿,12pts
2.遍历过的岛屿要用不同的标记,不然会在计算岛屿时重复计算
3.考虑一座岛屿遗留多个岛屿的情况,所以要用st确认是否已有遗留岛屿,否则36pts
4.这就是我自己的问题了,dfs没有写return直接爆了。
代码:
#include<iostream>
#include<algorithm>
#include<map>
using namespace std;
char a[1005][1005];
int dx[] = {0,0,-1,1};
int dy[] = {-1,1,0,0};
//可以利用这俩个数组加for循环简便遍历
int ans = 0;
bool st;
//标记这个岛屿是否有遗留
void dfs(int x, int y){
a[x][y] = '!';
//不同符号标记免得重复计算
int cnt = 0;
if (st) {
for (int i = 0; i < 4; i++) {
if (a[x + dx[i]][y + dy[i]] == '#' || a[x + dx[i]][y + dy[i]] == '!') {
cnt++;
}
}
if (cnt == 4) {
ans++;
//ans是不会被淹没的数量
st = false;
}
//四周都是岛屿,这块不会被淹没
}
//如果已经遗留就不用再算了
for (int i = 0; i < 4; i++) {
if(a[x + dx[i]][y + dy[i]] == '#'){
dfs(x + dx[i],y + dy[i]);
//遍历一整块岛屿
}
}
return ;
//要返回啊忘记设置返回了
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n;
cin >> n;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> a[i][j];
}
}
int sum = 0;
for (int i = 1; i < n - 1; i++) {
for (int j = 1; j < n - 1; j++) {
if (a[i][j] == '#'){
sum++;
st = true;
dfs(i,j);
//sum是岛屿数量
}
}
}
cout << sum - ans << endl;
return 0;
}
3.双指针
<1>(洛谷P9232)
【蓝桥杯 2023省A】更小的数
又没做出来呢
题解:
输入一个字符串num,计算有多少种不同的子串选择反转可以使改后小于num,位置不同作为不同方案。
看在当前位置的后面有几个数字比它小,反转此子串之后的数字必定比原来小,相等的情况要往中间继续查找,找到则ans++。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
string s;
cin>>s;
long long ans=0;
for(int i=0;i<s.length();i++){
for(int j=i+1;j<s.length();j++){
int l=i,r=j;
while(s[l]==s[r]&&l<r){
l++;
r--;
}
if(s[l]>s[r])ans++;
}
}
cout<<ans<<endl;
return 0;
}
4.线性DP
最长上升子序列模型属于线性DP。时间复杂度一般不高只有O(n)。定义一个一维数组即可,主要还是状态方程的判断。
dp[0]=dp[1]=0;
for(int i = 2; i<=n; i++){
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);
}
cout << dp[n] <<endl;
<1>(AcWing 898)
数字三角形
题解:
题目输入n表示三角形层数,第i行i个数字,要求给出从顶端连续走到底层的最大数字和。
一个线性DP,除了这种从下往上遍历的方法,也可以从上往上,还可以一维数组然后再进行比较,暂且不论那些了。具体见注释。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 505;
int n;
int sj[maxn][maxn];
//储存三角形数据
int dp[maxn][maxn];
//储存路径数据
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= i; j++){
cin >> sj[i][j];
}
}
for (int i = 1; i <= n; i++){
dp[n][i] = sj[n][i];
}
//最底层路径就是自己
for (int i = n - 1; i >= 1; i--){
for (int j = 1; j <= i; j++){
dp[i][j] = max (dp[i+1][j] , dp[i+1][j+1]) + sj[i][j];
//每个位置都得左右比较后赋值
}
}
cout << dp[1][1] <<endl;
//输出顶端路径最大数字和
return 0;
}
<2>(洛谷P9242)
【蓝桥杯 2023省B】接龙数列
训练的时候真不会写,就硬做,从前往后从后往前各遍历了一遍拿了三十分,然后赶紧来学线性DP了。
题解:
给出一个长度为n的整数数列A,要求最少删除几个数使其成为接龙序列。也就是上升子序列模版。
如果直接双重遍历找相同的值,那么时间复杂度为O(n2),会超时。所以可以考虑遍历以i结尾的最长子序列,会达成一个状态逐渐转移的结果。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100005;
int dp[12] = {0};
//用来储存以k结尾的数字的最长接龙子序列
long long A[N];
int n;
int l[N];
int r[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n;
for (int i = 0; i < n; i++){
cin >> A[i];
r[i] = A[i] % 10;
while( A[i] >= 10){
A[i] = A[i]/10;
}
l[i] = A[i];
}
for (int i = 0; i < n; i++) {
dp[r[i]] = max(dp[l[i]] + 1, dp[r[i]]);
}
int arr = 0;
for (int i = 0; i <= 9; i++) {
arr = max(arr, dp[i]);
}
cout << n-arr <<endl;
return 0;
}
5.区间DP
通常都是先枚举区间长度len,在分别枚举左端点i,分割点k,右端点j=i+len-1,时间复杂度为O(n3),很多讲解的例题都是一道很经典的石子合并。线性区间。
另一种情况是环状区间,那么将右端点设置到原区间的俩倍即可。以下代码就是环状区间。
for (int len = 1; len <= n; len++) {
//序列长度为n,len为区间长度
for (int i = 1; i + len - 1 <= 2 * n - 1; i++) {
//这是左端点啦
int j = i + len - 1;
//右区间范围最大
//此处可以加一些判断dp条件
for (int k = i; k < j; k++) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + weight[i, j]);
//状态转移方程
}
}
}
<1>(洛谷P9232)
【蓝桥杯 2023省A】更小的数
是的又是这一题我又来了,为了这一道题看了很多区间DP的例子,等会看看能不能做点别的,希望以后可以做出区间DP吧
题解:
输入一个字符串num,计算有多少种不同的子串选择反转可以使改后小于num,位置不同作为不同方案。
运用一个bool类型的区间DP,凡是>的直接赋值为1,=的则赋值为它的上一个状态,运作完后是1的ans++,计算符合条件的情况数目。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
bool dp[5005][5005];
//一开始设置成int很难写
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
string num;
cin >> num;
int n = num.length();
int ans=0;
for (int len = 2; len <= n; len++){
//翻转的区间长度
for (int i = 0; i + len - 1 <= n-1; i++){
//左端点,并确保右端点范围
int j = i + len - 1;
if (num[i] > num[j]){
dp[i][j]=1;
}
else if(num[i] == num[j]){
dp[i][j]=dp[i+1][j-1];
}
//以上都是在dp状态方程
if (dp[i][j] == 1) {
ans++;
}
}
}
cout << ans << endl;
return 0;
}
6.前缀和优化
<1>(洛谷P8649)
题解:
给出一个长度为N的数列和数字k,求其中有多少个区间是k倍区间。
不难想到要使用前缀和,然后就TLE 28pts,所以考虑优化,以数学思维,任意俩个相等余数的前缀数列相减都是k倍区间,ans加一下即可。附加一下数组开long long 才能过第三个案例。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
int a[100005];
long long to[100005] = {0};
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n,k;
cin >> n >> k;
long long sum = 0;
long long ans = 0;
to[0] = 1;
//从0开始
for (int i = 1; i <= n; i++){
cin >> a[i];
sum = sum + a[i];
to[sum % k]++;
//桶储存余数
sum = sum % k;
}
//不取余好像会爆long long,有个案例没过
for (int i = 0; i < k; i++) {
ans += ((to[i] * (to[i] - 1))/2);
}
//数学取俩个懂得都懂
cout << ans << endl;
return 0;
}
7.其它
<1>(SMU spring 天梯训练1 7—8)
阅览室 分数20
20分的题也不会了…简直不敢想象我改了多久这道题。几乎每个样例点都因为各种原因错了几次。
题解:
给出n组数据,每组包括数号,键值,对应的时间,求好好的借和归还的书的总数量和平均时间。
没有什么算法技巧具体见代码,主要说一下测试点。有关是以下四种类型的测试点。
1.非常普通甚至可能是样例,12分很好拿
2.多次归还数据,也就是多个E,只取第一个即可。还有多个S,只取最后一个。3分
3.包含一个从00:00开始的可行的数据,所以不能直接判断是否等于0,必须多加一个状态数组。2分
4.多个E的情况,只取第一个人。数据四舍五入与直接向上取整的问题也会导致。3分
代码:
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n;
cin >> n;
int sum = 0;
//第几天
int js = 0, sj = 0;
//借书的数量和总时间
int bh[1005] = {0};
//书的借还时间
bool lala[1005] = {false};
//书的借出状态
while (sum < n) {
int sh;
char jz;
cin >> sh >> jz;
int hh, mm;
char p;
cin >> hh >> p >> mm;
if (jz == 'S') {
//最后一次借出的人
bh[sh] = mm + 60 * hh;
lala[sh] = true;
} else if (jz == 'E' && lala[sh]) {
js++;
sj += (mm + 60 * hh - bh[sh]);
bh[sh] = 0;
lala[sh] = false;
//第一次归还的人
}
if (sh == 0) {
memset(bh, 0, 1005);
memset(lala, false, 1005);
//一天结束啦恢复原状态
sum++;
cout << js << ' ';
int ans;
if (js == 0) {
ans = 0;
} else {
ans = ((double) (sj) / (double) js + 0.5);
//答案要四舍五入不能直接ceil
}
cout << ans << endl;
sj = 0;
js = 0;
//恢复状态
}
}
return 0;
}
<2>(洛谷P8672)
【蓝桥杯 2018国C】交换次数
好奇怪的一道题,基本都是数学思维做的,代码没有什么难度
题解:
给出n个字符,包含A,B,T三种字母,要求相同字符挨在一起的最少的交换次数。
分为A,B,C区,只需要将A,B区全部交换正确即可,也就是A区非A加上B区非B减去A区B与B区A的较小值。t字符数组遍历六种情况,change函数进行上述运算,最后找到最小值即可。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
string s;
int ans = 0x3f3f3f3f;
char t[6][4] = {"ABT", "ATB", "BAT", "BTA", "TAB", "TBA"};
int changes(char A, char B, char C) {
int a = 0, b = 0, c = 0, numa = 0, numb = 0, wronga = 0, wrongb = 0, num = 0;
for (int i = 0; i < s.length(); i++) {
if (s[i] == A) a++;
else if (s[i] == B) b++;
else c++;
}
for (int i = 0; i < a; i++) {
if (s[i] != A) numa++;
if (s[i] == B) wrongb++;
}
for (int i = a; i < a + b; i++) {
if (s[i] != B) numb++;
if (s[i] == A) wronga++;
}
num += numa + numb - min(wronga, wrongb);
return num;
}
int main() {
cin >> s;
for (int i = 0; i < 6; i++) {
ans = min(ans, changes(t[i][0], t[i][1], t[i][2]));
}
cout << ans << endl;
return 0;
}
<3>(2023天梯赛L1-6)
不知道为什么我的代码有俩分一直不过,后来限时完看的别人的代码,至今不理解。
题解:
给出一个不含空格的string s,几组操作,若能找到前后字符串则插入。
有关string函数的一些应用掌握即可,代码如下。
#include<iostream>
#include<string>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
string s;
cin >> s;
int n;
cin >> n;
while (n--) {
int a,b;
cin >> a >> b;
string l,r;
cin >> l;
cin >> r;
string ls = s.substr(a-1, b-a+1);
s.erase(a-1,b-a+1);
string la = l + r;
int pos = s.find(la);
if(pos == -1){
s += ls;
}
else {
s.insert(pos+l.length(),ls);
}
}
cout << s << endl;
return 0;
}
三、总结
Segmentation fault(分段错误)其实就是数组越界了。
听的y总的课,加上自己上网学习的一些,并不是完全相同的写法。好吧其实课也没咋听,主要就是在看代码。
有关异或xor ,如果a xor b xor c=0,那么a xor b=c;如果a xor b=c,那么a xor c=b。异或满足交换律和结合律。线性基实在是没看懂,先放一放。
dp,dp,又是dp…浅看了一下dp类型不下十种,常用的有线性,背包,区间等等等好吧都很常用,而且变化非常多样,状态转移方程千奇百怪。