SMU-ACM集训队 2024 4th


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类型不下十种,常用的有线性,背包,区间等等等好吧都很常用,而且变化非常多样,状态转移方程千奇百怪。

  • 22
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值