SMU-ACM集训队2024 1st


The First Week

不积跬步,无以至千里。 ————荀子

一、前言

开学了,但是感觉每周写博客记录一下自己学了什么挺好的,看看我能不能多坚持一段时间吧,这次就是纯自觉啦。希望还是每周都有新东西。这周好像要打天梯选拔赛?稍微冲一下。
周六,开始回顾之前的题目,从第一次的洛谷开始,把题全补了,之前怎么都看不懂题解补不了题,现在可以了,虽然有些还是做不太出来,不过有进步啦嘿嘿。
周二打了一次天梯选拔赛,打得人不多看不太出水平其实,有一道题目题意理解错误,一直wa,后来自己补题一次过的,还是得多做一点。持续补题中…
又是周六,第二把天梯选拔赛。oi赛制,自我感觉还行吧,该对的都对了也就尽力了,还是有些函数和算法很不熟悉,有时间继续坎看课。
周日补题,敲代码ing“咔咔咔…”


二、算法

1.线性DP

<1>(洛谷 P8638)

不敢想象这题我琢磨了多久,感觉就是快做出来了又做不出来,看着题解研究这个dp状态方程,简直离谱。
题解:
题意要求给定一个字符串,最少增加多少个字符之后使其成为回文字符串。
查找LCS(最长公共子序列)是非常经典的一个题型,这里用dp查找给定字符串s和它的逆序s2的最长公共子序列,总长度减去其后就为答案。dp的难度主要在书写状态方程,这里也没有什么特殊性,我就不再过多说了。写了点注释。
这里有几个点导致我一直错误。
第一是数组开在主函数内,会一直报错Process finished with exit code -1073741571 (0xC00000FD),当在主函数内定义较大数组时,这些数组通常时在栈上分配内存,栈空间比堆小很多,可能会导致栈溢出。可以使用new和delete关键字来动态分配和释放内存,以避免栈溢出错误。
第二点是dp状态方程,这里写作f数组,必须从1遍历到n,因为dp状态层层推进,f[0][0]=0也有需要被用到,不然从0开始的话会导致找不到上一位,这个地方以后开数组也要注意。
第三点是我的问题了,因为从1开始开,但是我的s是从0开始,也就是赋值s1,s2的时候,写错了导致代码垮掉,啧,这个问题确实不应该。

代码:

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

char s1[1005],s2[1005];
int f[1005][1005];                                      //记录s1的前i个字符与s2的前j个字符的最长公共子序列长度
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    string s;
    cin>>s;
    int n=s.length();
    for(int i=1;i<=n;i++){
        s1[i]=s[i-1];
        s2[i]=s[n-i];
    }                                                    //s2为s1的逆向字符串
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){                           //dp状态方程
            if(s1[i]!=s2[j])f[i][j]=max(f[i-1][j],f[i][j-1]);
            else f[i][j]=f[i-1][j-1]+1;
        }
    }
    cout<<n-f[n][n]<<endl;                               //长度减去最长回文子字符串
    return 0;
}

2.树形DP

<1>(洛谷 P8625)

在做上题线性dp的时候,意外发现这题是树形dp,去研究了一下啊,几乎是照猫画虎了,还是没学会举一反三,但比之前好一点,能看懂了,这就是学会dfs了吧
题解:
题目给出n个节点的权值和n-1条双向边,要求计算节点集合S的最大值,可以是空集。
先是利用vector制造一颗树,以一个dfs计算每个节点的最大dp,进行比较即可。具体可看注释。
代码:

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

vector<int> to[100005];                           //vector数组中每一个节点可存放未知数量数字
long long int a[100005];                          //树上每个节点的权值
long long dp[100005];                             //每个节点的最大和
long long ans=0;                                  //赋初值为0,空集的情况
void dfs(int u,int fa){                           //dfs遍历每个节点,赋值dp与ans
    dp[u]=a[u];                                   //初始赋值为自身权值
    for(int v:to[u]){                             //虚拟代数v为to[u]的每个遍历
        if(v==fa)continue;                        //fa为其父节点,防止陷入死循环
        else dfs(v,u);                            
        dp[u]=max(dp[u],dp[u]+dp[v]);             //确认是否需要加上节点v的权值
    }                                             //其实只要是负数都舍去,正数都加入的啦
    ans=max(ans,dp[u]);
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    for(int i=1;i<n;i++){
        int u,v;
        cin>>u>>v;
        to[u].push_back(v);                       //vector的一种用法
        to[v].push_back(u);                       //可以记住一下,高光高光
    }
    dfs(1,0);                                     //从第一个开始遍历,父节点设为0
    cout<<ans<<endl;
    return 0;
}

3.二分算法

最后一次系统的二分,希望以后的二分题都能写对
题解:
不难看出l,r,mid的赋值状况以及循环条件均一致,l,r的变化情况也一致,判断条件以及输出一直在变,找到第一个数输出l,找到最后一个数输出r,找不到输出false,有些情况一定能找到。
代码:

    //找到arr中等于key的数字
    int l=0,r=n-1,mid;
    while(l<=r){
        mid=(l+r+1)/2;
        if(arr[mid]==key)return mid;           
        else if(arr[mid]>key)r=mid-1;
        else if(arr[mid]<key)l=mid+1;
    }
    return false;
    //找到arr中第一个等于key的数字
    int l=0,r=n-1,mid;
    while(l<=r){
        mid=(l+r+1)/2;      
        if(arr[mid]>=key)r=mid-1;
        else if(arr[mid]<key)l=mid+1;
    }
    if(l<n && arr[l]==key) return l;
    //查找第一个大于等于key的数字就不需要“arr[l]==key”的判断条件
    else return false;
    
    //找到arr中最后一个等于key的数字
    int l=0,r=n-1,mid;
    while(l<=r){
        mid=(l+r+1)/2;      
        if(arr[mid]<+key)l=mid+1;
        else if(arr[mid]>key)r=mid-1;
    }
    if(r>=0 && arr[r]==key) return r;
    else return false;
    //查找数组中最后一个小于等于key的数字
    int l=0,r=n-1,mid;
    while(l<=r){
        mid=(l+r+1)/2;
        if(arr[mid]<=key)l=mid+1;
    //查找数组中最后一个小于key的数字就是将“<="改成”<“
        else r=mid-1;
    }
    return r;
    
    //查找数组中第一个大于key的数字
    int l=0,r=n-1,mid;
    while(l<=r){
        mid=(l+r+1)/2;
        if(arr[mid]>key)l=mid+1;
        else r=mid-1;
    }
    return l;
    

<1>(洛谷 B3691)

记不太清这题有没有补过了,反正再做一次还是很懵,看了解析自己又写了一遍,主要是想不到这题是二分,思路还是很清晰的。

4.位运算

二进制枚举
1<<m :意味着1向左移动m位,右侧补足0,即2的m次方。如1<<3意味着2的3次方即8,因为1000的十进制表示是8。
1>>m :是往右位移m位,如1001往右边位移1位就是0100,即右边去掉几位后左侧补0。
二进制枚举的思想是选与不选的暴力思想,选与不选分别是0与1的情况,而总共有m个数字,每种数字有俩个情况,所以需要2的m次方的情况数组,也就是上面的符号,二进制直接位移即可。二进制枚举是一种暴力枚举,相较于dfs,其没有剪枝功能,必须遍历完所有的2的n次方种情况,相对时间复杂度骄傲,但是写起来相对简单,在赛场上较节约时间。

<1>(洛谷 P1036)

很简单的板子,之前还用dfs写过一次,就不写题解啦,在补周二练习赛的题的时候发现了这个陌生又熟悉的名词,就先去学习了一下,做这题了解了一下基本概念,据说是基础,果然还是欲速则不达啊。
代码:

#include <iostream>

using namespace std;

int n,k;
int x[25];
bool is_prime(int ans){
    if(ans==1||ans==2)return true;
    for(int i=2;i*i<ans;i++){
        if(ans%i==0)return false;
    }
    return true;
}
int main(){
    cin>>n>>k;
    for(int i=0;i<n;i++){
        cin>>x[i];
    }
    int t=0;
    for(int i=0;i<(1<<n);i++){
        int cnt=0;
        int ans=0;
        for(int j=0;j<n;j++){
            if(i>>j&1)               //表示i在第j位上是否是1
                {
                cnt++;ans+=x[j];
                }
        }
        if(cnt!=k)continue;          //没取出k个数字直接排除情况
        if(is_prime(ans))t++;
    }
    cout<<t<<endl;
    return 0;
}

<2>(天梯选拔赛(一)B 孵化小鸡)

这有个问题就是我以后该怎么找到题源呢,这题很想补可是它的题源放不上来,要不我题意写仔细一点吧。另外比赛的时候确实是一点思绪也没有,补题的时候看了会代码就懂了,没有什么特别难度好像,比较经典的题。
题解:
题意为给出俩个正整数N,M,在N行中分别输入a,b,m,分别表示在a到b的范围内鸡蛋的温暖值是m。在M行中分别输入l,r,k,p,分别表示在l到r的区域内温度上升k需要p元。要求找出最少的花销使得所有鸡蛋都可以正常孵化,M个暖源。
运用了桶排序的思想,将每个位置的鸡蛋算作一个桶,后运用二进制枚举的思想,一一列举情况使用这个暖源以及不使用,将所有满足条件的比较取得最小值。
思想大概是这样,就不写注释了。
代码:

#include<iostream>

using namespace std;

int a[22],b[22],m[22];
int x[105]={-1};
int l[12],r[12],k[12],p[12];
int N,M;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    cin>>N>>M;
    for(int i=0;i<N;i++){
        cin>>a[i]>>b[i]>>m[i];
        for(int j=a[i];j<=b[i];j++){
            x[j]=m[i];
        }
    }
    for(int i=0;i<M;i++){
        cin>>l[i]>>r[i]>>k[i]>>p[i];
    }
    int ans=1000000;
    for(int i=0;i<(1<<M);i++){
        int t[105]={0};
        int sum=0;
        for(int j=0;j<M;j++){
            if((i>>j)&1){
                sum+=p[j];
                for(int ls=l[j];ls<=r[j];ls++){
                    t[ls]+=k[j];
                }
            }
        }
        bool st=true;
        for(int i=0;i<=102;i++){
            if(x[i]!=-1&&x[i]>t[i]){
                st=false;
                break;
            }
        }
        if(st)ans=min(ans,sum);
    }
    cout<<ans<<endl;
}

<3>(天梯选拔赛(二)C—04—L1—8)

2024常熟理工学院团体程序设计天梯赛选拔L1—8 该加训啦
该加训啦 加训啦 训啦 啦。这题代码非常简单,逻辑也很易懂,当时不少人都做出来了,但是我实在不太知道或与亦或,所以怎么都写不出来,后来拿了一分吧,呵呵,前缀和倒是挺了解的,就不开它的模块了(希望吧),异或符号都差点写不出来,要不是后面有提示,一点也不会呢。
题解:
题意函数过于复杂,不写了,上张真值表。

aba&ba|ba xor b(异或)
00000
01011
10011
11110

可以得到(a&b) xor (a∣b)=a xor b

代码:

#include<iostream>

using namespace std;

int main() {
   int n;
   cin >> n;
   long long int x[n];
   int s[n];
   for (int i = 1; i <= n; i++) {
       cin >> x[i];
       s[i] = s[i - 1] xor x[i];              //前缀和
   }                                          //x[I] xor 0=x [I]
   int m;
   cin >> m;
   for (int i = 0; i < m; i++) {
       int l, r;
       cin >> l >> r;
       int t = s[r] xor s[l - 1];          //s[r] xor s[l-1]等于s[l]到s[r]之间所有元素的异或结果
       cout << t << endl;
   }
}

5.DFS算法

<1>(天梯选拔赛(二)C—04—L2—2)

2024常熟理工学院团体程序设计天梯赛选拔L2—2 swj学长的精灵融合
其实这题的案例我是相当不理解,要么不过要么全过,可惜选拔赛不能看样例。补题的时候做了好几次一直都不过,加了几个sum函数储存经验值之后忽然就过了,我也是百思不得其解,只能说下次小范围数据还是存数组吧,还用了vector,这个还挺实用的。另外这题试着标准化了一下,用了一些define和const,非常之不习惯。
题解:
题意要求输入俩个整数最终精灵编号x和融合关系总数m,输入m行a,b,c,d。c是种类,不同种类精灵升级所需要的经验值不同,d是所需达到的等级,a是合成的精灵,b是自己的编号。
这题的思路重点在于只算上自己目标精灵树的经验值,所以用到dfs层层遍历标记,要注意vector可储存任意数量数字,所以可以用来一次记录所需精灵。写过挺多dfs题目了,这题思路大致相同,不过多解释了。
代码

#include<iostream>
#include<vector>
using namespace std;

#define int long long

const int N = 1e5 + 10;
int x, m;
int res = 0;
int zl[N];
int dj[N];
int sum1[N], sum2[N], sum3[N];
std::vector<int> p[N];
int vis[N] = {0};

void dfs(int u) {
 vis[u] = 1;
 for (int i = 0; i < p[u].size(); i++) {
     int q = p[u][i];
     if (!vis[q]) {
         if (zl[q] == 1) {
             res += sum1[dj[q]];
         }
         if (zl[q] == 2) {
             res += sum2[dj[q]];
         }
         if (zl[q] == 3) {
             res += sum3[dj[q]];
         }
         dfs(q);
     } else continue;
 }
}

signed main() {
 ios::sync_with_stdio(false);
 cin.tie(nullptr);
 cout.tie(nullptr);
 int ans = 1, idx = 0;
 for (int i = 2; i <= 100; i++) {
     sum1[i] = ans + idx;
     ans = sum1[i];
     idx += 1;
 }

 ans = 1, idx = 0;
 for (int i = 2; i <= 100; i++) {
     sum2[i] = ans + idx;
     ans = sum2[i];
     idx += 2;
 }

 ans = 1, idx = 0;
 for (int i = 2; i <= 100; i++) {
     sum3[i] = ans + idx;
     ans = sum3[i];
     idx += 5;
 }
 cin >> x >> m;
 for (int i = 0; i < m; i++) {
     int a, b, c, d;
     cin >> a >> b >> c >> d;
     p[a].push_back(b);
     zl[b] = c;
     dj[b] = d;
 }
 dfs(x);
 cout << res << endl;
 return 0;
}

6.其它

<1>(string函数)

find,rfind,和replace函数.
(r)find函数:用于string中,找到即返回位置或元素,找不到则返回string::npos即-1;

  string str = "Hello, world!";
  auto it = find(str.begin(), str.end(), 'w');
  if (it != str.end())
      cout << distance(str.begin(), it) << '\n'; //输出7
  else
      cout << "Not found\n";

  int pos=str.find('w');
  if(pos==-1)
  cout << "Not found\n";
  else cout<<pos;                               //输出字符串左侧位置7

 int poss=str.rfind("world");           //从右侧开始匹配,输出从右往左找到的
                                      //第一个符合条件的字符串的最左侧字符的位置
 
 int ppos=str.find('o',5)                  从第五个位置开始寻找字符o的位置

replace函数:
string& replace (size_t pos, size_t len, const string& str);
str=str.replace(str.find(“a”),2,“#”); //从第一个a位置开始的两个字符替换成#
s=s.replace(‘.’,“.xixixixi.”);
replace函数是对原函数进行修改,不用再度赋值给s。

<2>(质因数分解)

实在没什么好说的,记录一下这一小段我研究了很久的代码,可用于质因数分解求因子数。
代码:

int calc(int x) {
 int ans = 1;
 for (int i = 2, cnt; i * i <= x; i++) {
     if (x % i) continue;
     int cnt = 0;
     while (x % i == 0) {
            cnt++;
            x /= i;          //相同的因子,如2
     }
     ans *= (cnt + 1);       //加一是为了要算上最后的数字
 }
 if (x > 1) ans *= 2;        //x本身也是个因子
 return ans;
}

三、总结

有十一种dp诶,动态规划也太难了,而且跟贪心竟然还不一样,感觉有点分不太清。
开始看之前的BNU_ACM了,讲得超级清楚,寒假怎么就不好好看呢。哦还是要交周报,继续外力坚持了。
这周一直在比赛和补题,看视频学习的时间其实相对较少,还是得继续学的,很多算法还是很不清楚,下周要平衡一下时间,不过大多数的题目其实半暴力半算法还是做出来了的,但是话费了更多时间,有一点得不偿失。
另外发现我补题的时候好像都是在做一些之前已经知道的,有些思路的题目,那我不会一直在做同一个类型的题吧,还是得补一些平时不做的赛场上也没思路的题,说不定下次就会了呢。
根本没时间打cf和牛客,什么时候才能给我的cf上个分啊,时间也太难调和了,后悔寒假没有多打了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值