by wjl

2021.1.25-2021.1.31

  • 2021.1.25
    • 某大学ACM实验室寒假新生培训Day3:算法基础一(模拟、枚举、递推、递归)
      • 模拟
        • 例题:回文串
        • 例题:旋转吧!雪月花
      • 枚举
        • 例题:百钱买百鸡
        • 例题:This Year‘s Substring
        • 例题:校园活动
      • 递推和递归
        • 递推
        • 递归
        • 例题:阶乘
        • 例题:兔子 兔子
        • 例题:碎梦
  • 2021.1.26
    • 某大学ACM实验室寒假新生培训Day4:算法基础二(二分)
      • 引子
      • 二分讲解
        • 例题:跳石头
        • 例题:借教室
        • 例题:寻找段落
  • 2021.1.27
    • 某大学ACM实验室寒假新生培训Day5:数学入门
      • 前置知识
        • 先定义一些符号
        • 快速幂
      • 欧几里得算法
        • 关于取整的经典题目
        • 取整与取模的转换
  • 2021.1.28
    • 某大学ACM实验室寒假新生培训Day6:数据结构入门
        • 理解的例题:洗盘子
        • 顶端操作
        • 例题:电梯问题
        • 例题:平衡的括号
        • vj例题:A-Rails(stack)
        • 例题:排队
        • 单调栈
      • 队列
        • 例题:约瑟夫问题
        • 例题:求细胞数量
        • 单调队列
      • 并查集
        • 例题:家族问题
        • 模板题
        • 例题:修复公路
  • 2021.1.29
    • 某大学ACM实验室寒假新生培训Day7:动态规划入门
      • 01背包
      • 完全背包
      • 优化
      • 多重背包
        • solution1:
        • solution2:
  • 2021.1.30-2021.1.31
    • 某大学ACM实验室寒假新生培训Day6:数据结构入门(补充)
        • 二叉树
        • 堆的操作
          • push
          • top
          • pop
        • 模板和代码

021.1.25-2021.1.31)

2021.1.25

某大学ACM实验室寒假新生培训Day3:算法基础一(模拟、枚举、递推、递归)

什么是程序
算法+数据结构=程序
什么是算法
做饭是算法,把大象放进冰箱是算法,为了完成某一件事,需要执行的步骤
什么是数据结构
鸡蛋可以认为是一种母鸡封装好的数据结构,即一些数据的集合
常见算法:模拟、枚举、递推、递归、二分、贪心、深度优先搜索(dfs)、广度优先搜索(bfs)

模拟

顾名思义,就是写程序来模拟题目中所要求的操作,只需要按照题面的意思来写即可。

例题:回文串

题目描述
输入一个字符串,判断这个字符串是不是回文串,如果是,输出“YES”,否则输出“NO”
“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串,而“windows”不是
输入
只有一行,包含一个字符串,长度不超过1000
输出
“YES”或“NO”
样例输入
level
样例输出
YES
下面就是AC的代码了

#include<bits/stdc++.h>
using namespace std;//std::cin>>s;
int main()
{string s;cin>>s;int n=s.length();bool ok =true;for(int i=0;i<n;i++){if(s[i]!=s[n-1-i]){ok=false;}}cout<<(ok?"YES":"NO")<<"\n";
//三目运算符  判断?输出一:输出二  如果判断值为真,输出一,如果判断值为假,输出二return 0;
}

三目运算符用法详解

例题:旋转吧!雪月花

题目描述
这天,夜夜捡到了一个长为 nn 的数组,贪玩的她把数组首尾拼接了起来,并想从任意位置开始遍历数组一周。
输入
第一行给出数组 aa 的大小 n。
第二行给出 n个数。
第三行给出遍历的起点 b 。
(1≤n≤100, 1≤ai≤1000, 1≤b≤n)
输出
从起点遍历数组一周的结果。
样例输入

5
2 4 3 1 5
2

样例输出

4 3 1 5 2

下面就是最开始AC的代码了

#include<bits/stdc++.h>
using namespace std;
int main()
{int n;cin>>n;int a[n]{};for(int i=0;i<n;i++){cin>>a[i];}int b;cin>>b;//第b个数,下标是b-1--b;//转换下标for(int i=b;i<n;i++){//遍历b到n-1cout<<a[i]<<" ";}for(int i=0;i<b;i++){cout<<a[i]<<" ";}return 0;
}

下面是优化之后的代码,用到了取余的思路

#include<bits/stdc++.h>
using namespace std;
int main()
{int n;cin>>n;int a[n]{};for(int i=0;i<n;i++){cin>>a[i];}int b;cin>>b;//第b个数,下标是b-1--b;//转换下标for(int i=0;i<n;i++){cout<<a[(b+i)%n]<<" ";//当b+i=n之后,就又会从0遍历到b}return 0;
}

枚举

指依据题意,枚举所有可能的状态,并用题目中给定的条件来判断哪些是需要的,哪些是不需要的。
枚举是初等题目中最常用的思想。

例题:百钱买百鸡

题目描述
我国古代数学家张丘建在《算经》一书中提出的数学问题:鸡翁一值钱五,鸡母一值钱三,鸡雏三值钱一。百钱买百鸡,问鸡翁、鸡母、鸡雏各几何?

下面是最开始写出来的AC代码

#include<bits/stdc++.h>
using namespace std;
int main()
{for(int i=0;i<=100;i++){//公鸡的个数for(int j=0;j<=100;j++){//母鸡的个数for(int k=0;k<=100;k++){//小鸡的个数,小鸡的只数一定是三的倍数if(k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){cout<<i<<' '<<j<<' '<<k<<"\n";}}}}return 0;
}
/*
总计算次数:100*100*100=10^6
<=10^8
*/

下面就是逐步优化代码的过程了
减少枚举范围
公鸡个数不会超过100/5=20只
母鸡个数不会超过100/3=33只
小鸡个数不会超过300只,又因为是买百鸡,则不会超过100只

#include<bits/stdc++.h>
using namespace std;
int main()
{for(int i=0;i<=20;i++){//公鸡的个数for(int j=0;j<=33;j++){//母鸡的个数for(int k=0;k<=100;k++){//小鸡的个数,小鸡的只数一定是三的倍数if(k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){cout<<i<<' '<<j<<' '<<k<<"\n";}}}}return 0;
}
/*
总计算次数:20*33*100=60000
<=10^8
*/

减少枚举变量
小鸡的个数=100-i-j

#include<bits/stdc++.h>
using namespace std;
int main()
{for(int i=0;i<=20;i++){//公鸡的个数for(int j=0;j<=33;j++){//母鸡的个数int k=100-i-j;if(k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){cout<<i<<' '<<j<<' '<<k<<"\n";}}}return 0;
}
/*
总计算次数:20*33=660
<=10^8
*/

进一步减少枚举变量
i+j+k=100
5i+3j+k/3=100
所以
15i+9j+k=300
14i+8j=200即7i+4j=100

#include<bits/stdc++.h>
using namespace std;
int main()
{for(int i=0;i<=20;i++){//公鸡的个数int j=(100-7*i)/4;int k=100-i-j;if((100-7*i)%4==0 and j>=0 and k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){//判断条件多了,注意不要少条件cout<<i<<' '<<j<<' '<<k<<"\n";}}return 0;
}
/*
总计算次数:20
<=10^8
*/

递增的倍数增加
由公鸡和母鸡的只数关系可以知道,公鸡的只数一定是四的倍数

#include<bits/stdc++.h>
using namespace std;
int main()
{for(int i=0;i<=20;i+=4){//公鸡的个数int j=(100-7*i)/4;int k=100-i-j;if((100-7*i)%4==0 and j>=0 and k%3==0 and 5*i+3*j+k/3==100 and i+j+k==100){cout<<i<<' '<<j<<' '<<k<<"\n";}}return 0;
}
/*
总计算次数:6
<=10^8
*/

例题:This Year‘s Substring

一个字符串能否去掉中间某一段,使得余下的首尾相接后刚好为 2021 。
如:200021 去掉中间的 00 后可以得到 2021
原题链接 这两个题有些许不同

#include<bits/stdc++.h>
using namespace std;
int main()
{string s;cin>>s;int n=s.length();bool ok = false;//枚举去掉的字符串的起点和终点for(int i=0;i<n;i++){//去掉的起点for(iny j=i;j<n;j++){//去掉的终点//去掉字符串的下标范围为[i,j]string t=s.suber(0,i)+s.suber(j+1);//suber的用法在1.24有提到if(t=="2021"){ok=true;}}}cout<<(ok?"Yes":"No")<<"\n";return 0;
}

优化后

#include <bits/stdc++.h>
using namespace std;
int main() {string s;cin >> s;int n = s.length();bool ok = false;for (int i = 0; i <= 4; i++) { //枚举余下的首端的长度string t = s.substr(0, i) + s.substr(n - (4 - i));// s.substr(0, 1) + s.substr(n - 3);// 0 1 ... n - 3  n - 2  n - 1if (t == "2021") {ok = true;}}cout << (ok ? "Yes" : "No") << "\n";return 0;
}/*i   j0   0 ~ n - 1   n1   1 ~ n - 1   n - 1...n - 1  n - 1 ~ n - 1   1总计算次数:4
*/

例题:校园活动

原题链接
将一个数字字符串分为连续的一些组,使得每个组内的数字之和相等,最多可以分为多少组。
如:31113 最多可以分为 3 组:3 、 111 、 3 。
求和:3 +1+1+1+ 3 = 9 ,9 为 3 的倍数
枚举剩下多少组,范围为:[1, n]

#include<bits/stdc++.h>
using namespace std;
int main()
{int n;cin>>n;string s;cin>>s;int sum=0;for(int i=0;i<n;i++){//对字符串进行求和sum+=s[i]-'0';}for(int i=n;i>=2;i--){if(sum%i==0){bool ok=true;//能否恰好分为i组int ave=sum/i;//每组的和int cur=0;//当前组的和for(int j=0;j<n;j++){cur+=s[j]-'0';if(cur>ave){ok=false;}else if(cur==ave){cur=0;}}if(ok){cout<<i<<"\n";return 0;}}}cout<<-1<<"\n";return 0;
}

递推和递归

递推

通常用于序列计算,序列中的每一项依赖于前面一个或多个项的值。

递归

一个函数自己调用自己即是递归。

两者的区别
二者对问题的求解方式本质上是一样的,不同的是求解次序:
从前往后求解是递推,从后往前求解是递归;
由已知到未知是递推,由未知到已知是递归。

例题:阶乘

计算20内某个数的阶乘

递推的写法

#include<bits/stdc++.h>
using namespace std;
int main()
{int n;cin>>n;long long fac[n+1]={};fac[0]=fac[1]=1;for(int i=2;i<=n;i++)fac[i]=fac[i-1]*i;cout<<fac[n]<<"\n";return 0;
}

递归的写法

#include<bits/stdc++.h>
using namespace std;long long fac(int n){if(n==0)return 1;return n*fac(n-1);
}int main(){int n;cin>>n;cout<<fac(n)<<"\n";return 0;
}

例题:兔子 兔子

开始时有一对小兔,小兔出生后第三个月起每个月都会再生一对小兔,输入n,问第n个月有多少对兔子?
第一个月:1对
第二个月:1对
第三个月:2对
第四个月:3对
第五个月:5对
……类似于斐波那切数列
f[n] = f[n-1] + f[n-2]
//第 n 个月的兔子数 = 上一个月的兔子数(f[n-1]) + 新生的兔子数(f[n-2])
f[n] //第 n 个月有多少对兔子

递推的解法

#include<bits/stdc++.h>
using namespace std;
int main()
{int n;cin>>n;long long f[n+1]{};f[1]=f[2]=1;for(int i=3;i<=n;i++)f[i]=f[i-1]+f[i-2];cout<<f[n]<<"\n";return 0;
}

这是递归的写法

#include<bits/stdc++.h>
using namespace std;
long long f(int n){if(n==1 or n==2)return 1;return f(n-1)+f(n-2);
}int main()
{int n;cin>>n;cout<<f(n)<<"\n";return 0;
}

例题:碎梦

窗台上有 n 堆纸飞机,数目分别为 1 , 2 , … , n 。
每次可以选择一个数 x ,然后从所有纸飞机数目大于等于 x 的堆中各掷出 x 只纸飞机。
问最少要选择多少次才能将所有纸飞机掷出?
原题链接

递归解法

/*n = 51 2 3 4 5x = 1   0 1 2 3 4x = 2   1 0 1 2 3x = 3   1 2 0 1 2x = 4   1 2 3 0 1x = 5   1 2 3 4 0n = 61 2 3 4 5 6x = 1   0 1 2 3 4 5 x = 2   1 0 1 2 3 4x = 3   1 2 0 1 2 3x = 4   1 2 3 0 1 2x = 5   1 2 3 4 0 1x = 6   1 2 3 4 5 0
*/#include <bits/stdc++.h>
using namespace std;int solve(int n) {if (n == 1) {return 1;}if (n % 2 == 1) {return 1 + solve((n - 1) / 2);} else {return 1 + solve(n / 2);}
}int main() {cout << solve(5) << "\n";return 0;
}/*n = 3   1 + solve(2)n = 2   1 + solve(1)
*/

2021.1.26

某大学ACM实验室寒假新生培训Day4:算法基础二(二分)

引子

给n个数,m次询问,每次询问给一个数a,找到n个数中比a小的最大的数,数据保证这样的数存在。
不会二分的人
!数组操作 每次询问的时候扫一遍原数组 时间复杂度O(n×m).
! 排序离线,只需要把数组排序,查询值也排序,就可以扫一遍! 时间复杂度O(nlogn)。
!stl(set)…….

二分讲解

我们先将数组进行排序,对于每次询问假设答案的下标在k,那么k之前的所有数必定满足其小于a。K之后的所有数必定满足k大于等于a。 我们把满足 小于a 这个作为条件,如果满足,就定义其为合法(1),不满足就定义其为非法(0),那么序列应该是这个样子
111……111000……000
我们的目的就是找到最大的1所在的位置。

体现在代码上,可以先写一个check函数

bool check(int id)
{if(w[id]<a)return 1;return 0;
}

如果n很大的话,通过枚举找最后一个1的过程就很慢,但我们可以通过二分来进行查找。

void work1()
{int ls =1,rs=n+1;while(ls+1<rs){int mid=(ls+rs)/2;if(!check(mid))rs=mid;else ls=mid;}printf("%d\n",w[ls]);
}

简单来说,维护一个区间使答案必定在这个区间内,然后继续维护,使得左端点必定合法(check为1),右端点必定不合法(check为0)然后不断通过取左右端点的中点的方式来每次将这个区间缩短一半,这样当ls+1=rs的时候,左端点就是答案。
其实就是一个左闭右开的区间。
简单来说,如果我们已经实现了check函数,那么无非是下列两种情况
11111110000000 让你求最大的1的位置
00000001111111 让你求最小的1的位置
第一个就是上述方法(左闭右开)
第二个只需要维护ls永远非法,rs永远合法即可(左开右闭)。
然而二分的难点其实在于check函数的实现,即怎样判断当前位置的数是否满足题目要求。
这个大家做题的时候应该会有所感悟。

例题:跳石头

数轴上有n个石子,第i个石头的坐标为Di,现在要从0跳到L,每次条都从一个石子跳到相邻的下一个石子。现在FJ允许你最多移走M个石子,问移走这M个石子后,相邻两个石子距离的最小值最大是多少。
(N<=50000,L<=1e9)
题解
据说“最小值最大”和“最大值最小”是基本可以判断二分法的
如果一个距离满足题意,则更小的距离肯定满足,所以答案满足单调性,考虑二分答案。
那么我们在跳跃距离[0,L]之间二分枚举一个最小跳跃距离作为答案mid进行判断,看是否符合最多移走m个的限制,如果符合,那么mid可能就是答案,但也可能还存在更大的答案,所以要向右二分查找。如果不行,那肯定大了,向左二分查找。
判定直接O(N)的贪心即可
总时间复杂度为NlogL
在网上找到的题解链接

例题:借教室

N天内有m份借教室的订单,学校第i天有ri个教室可借,第i每份订单显示从第si~ti天需要借di个教室,请你按照订单先后顺序分配教室,如果能够完成分配输出0,否则输出第一个无法满足的订单编号。
1≤n,m≤1e6,0≤ri,dis≤1e9
题解
首先考虑暴力方法:枚举每个订单,对订单内的每天更新剩余教室数量,如果出现负数,即表示该订单不可行。N×M
对于会线段树的人来说这就是一道送分题,但不幸的是只能90分。
而且线段树在12年的时候属于高端算法,没几个人会
且发现订单满足单调性,可以二分,判定可以利用订单的区间更新特性,求出每天需要教室数的差分数组,求前缀和即可判定出教室是否够用。MlogN。

例题:寻找段落

给定一个长度为n的序列a_i,定义a[i]为第i个元素的价值。现在需要找出序列中最有价值的段落。段落的定义是长度必须大于s的连续序列。价值=段落总价值/段落长度。
n<=100000,1<=S<=T<=n,-10000<=价值指数<=10000。
题解
考虑二分一个平均值mid,我们将a全部减去mid,问题转化为判断是否存在一个长度在s~t范围内的区间,它的和为正,如果有说明还有更大的平均值。
即求sum[i]-min(sum[i-s]…sum[0])是否大于0

2021.1.27

某大学ACM实验室寒假新生培训Day5:数学入门

前置知识

先定义一些符号

⌊x⌋表示x向下取整
⌈x⌉表示x向上取整
[esp]其中exp代表一个表达式,如果表达式为真,值为1,如果表达式为假,值为0
gcd(x,y)表示x与y的最大公约数
lcm(x,y)表示x与y的最小公倍数
d|n表示d整除n,n是d的倍数
a ≡ b(mod n)表示a,b在模n的意义下同余

快速幂

求a^b mod m的值,且b<=10^18

求a1,a2,a4,a8,a^2n
把b进行二进制分解
若b=5,则b=101B,a5=a1 × a^4

//b=5
int quick_pow(int a,int b){int ret=1;while(b){//满足b不为0if(b&1)ret=ret*a%mod;//a^1 ret*=a^1b>>=1;a=a*a%mod;//之后a^2  a^4 ret*=a^4,之后b就是0了,a^8,所以是a^1+a^4}return ret;
}

a+b %m=(a%m+b%m)%m
a×b %m=(a%m×b%m)%m
a^b %m=a%m^(b%m)

pow进行的是浮点运算

欧几里得算法

欧几里得算法,就是求gcd(x,y)。在辗转相除法上进行改进。
假设gcd(x,y)=d,且d|x,d|y等价于d|(x-y),d|x。所以gcd(x,y)=gcd(x-y,y)=gcd(x-2×y,x)=gcd(x-k×y,x)=gcd(y,x%y)。显然每次取模后数字的大小至少折半,所以复杂度是低于O(log max(x,y))级别的
gcd(x,0)=x

int gcd(int x,int y){if(!y)return x;//函数的终止条件是y=0return gcd(x%y,x);
}

关于取整的经典题目

即求:n/1+n/2+n/3+……+n/n

//超时代码
for(int i=1;i<=n;++i)ans+=n/i;

n/i (i<=n)取整,最多一共有根号n种取值
sqrt(n)<n
情况一:当i<=sqer(n)
n/1,n/2,n/3,n/4……n/sqrt(n)(最多有sqrt(n)种)
情况二:当i>sqrt(n)
可得i/sqrt(n)<sqrt(n) i/aqrt(n),i/(sqrt(n)+1)……i/n
n/i<sqrt(n) n<sqrt(n)×i sqrt
n/sqrt(n) n/sqrt(n)+1 n/nsqrt[1,sqrt(n)]

则总共有sqrt(n)种
每种取值的i一定是一个连续的区间

/l和r是左区间端点右区间端点
for(int l=1,r=1;l<=n;l=r+1){r=n/(n/l);ans+=n/l*(r-l+1);
}

如何找左区间端点,右区间端点?
左区间端点=上一个区间右区间端点+1
着重于找右区间端点
首先求出n/l=1 ,再次,x=n/l找r使n>=x×r,r=n/x向下取整

取整与取模的转换

cin>>n;
long long ans = 0;
for(int l=1,r=1;l<=n;l=r+1){r=n/(n/l);ans+=(r-l+1)*(r+l)/2*n/l*(r-l+1);//l,l+1,l+2……r
}
cout<<n<<endl;

2021.1.28

某大学ACM实验室寒假新生培训Day6:数据结构入门

数据结构是组织数据的结构。用来方便我们的操作管理数据。数组是一种简单的数据结构(顺序存储结构的线性表)。

生活中有大量遵循“后进先出”规则的结构,我们将之抽象为
栈(stack)支持如下操作:查询栈顶,弹出栈顶,向顶部压入元素
模拟盘子、模拟电梯,这两种任务只需要用栈就能完成。

//定义栈
#include<bits/stdc++.h>
using namespace std;
struct node{int x,y;
};
strack<node>a;
int main()
{return 0;
}

理解的例题:洗盘子

大清洗期间,R 君每天要洗盘子。盘子总是堆成一叠,已知 R 君总是取出最顶端的盘子,拿去洗。
其他人总是把需要清洗的脏盘子,放在最顶端。
给所有盘子编号。现在考虑以下操作:放进1,放进2,清洗, 放进3,放进4,清洗,清洗,放进5,清洗,清洗。
问:遭到清洗的盘子顺序。
2 4 3 5 1 ,就是简单的栈的问题

顶端操作

永远在顶端进行操作,在顶端加入,在顶端弹出,它的特性就是:后进先出

例题:电梯问题

卢比扬卡包吃包住,所以那里的电梯经常人满为患。
依据常识,当超载铃响时,最晚进来的人应该退出。如果要模拟 这台电梯(进入人、退出人),必然也会满足后进先出规则。
因此,挤电梯和洗盘子遵循同一套规则,洗盘子的模拟程序也能 用来模拟卢比扬卡的电梯。
代码实现
起一个数组a,用来直接存放栈里面的每一个元素。记录tot(栈 里面的元素个数)。
查询栈顶:返回a[tot]
弹出栈顶:tot–
向顶部压入元素:a[++tot] = x

#include<stack>
using namespace std;void caic()
{stack<int>s;//定义一个栈,存储int型for(int i=1;i<=s;i++)s.push(i);//压入栈while(!s.empty())//当站非空时{printf("%d\n",s.top());//查询栈顶s.pop();//弹出}
}

STL 的栈支持其他各种数据类型、支持用户自己的struct.
空间占用是动态的;比起自己手写的栈稍慢。

例题:平衡的括号

原题链接
思路
维护一个栈,初始为空,从左到右扫描括号序列:

  • 若为左括号,则压入栈
  • 若为右括号,则判断栈顶是否与之成对。如果成对,则弹出栈顶,否则报告错误。
    扫描完成后,若栈中还剩下元素,则报告错误,否则报告序列合法
#include<bits/stdc++.h>
#define maxn 1000010
using namespace std;
int n,tot;
char s[maxn],a[maxn];
int main()
{scanf("%d",&n);while(n--){tot=0;scanf("%s",s+1);int le=strlen(s+1),ans=1;for(int i=1;i<=len;i++){if(s[i]=='('||s[i]=='['){++tot;a[tot]=s[i];}if(s[i]==')'){if(a[tot]=='['||tot==0)ans=0;elsetot--;}if(s[i]==']'){if(a[tot]=='('||tot==0)ans=0;elsetot--;}}if(ans)printf("Yes\n");else printf("No\n");}return 0;
}

vj例题:A-Rails(stack)

原题链接
需要模拟一个栈,试图让它的出栈顺序符合题目所给。
维护一个栈,初始为空。以变量tot 记录“入过栈的最大数”。 依次考虑目标序列中的每一个元素x:
1 若x 小于tot。
如果栈顶不为x,则报告非法。否则,弹出栈顶。
2 若x 大于等于tot,则把[tot, x] 内的所有数依次压进栈,再 弹出栈顶,tot 改为x。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int k,n,a[1005];
int p[1005],t;
bool ok;
int main(){while(cin>>n&&n!=0){abc://abc在这!while(true){memset(a,0,sizeof(a));memset(p,0,sizeof(p));//因为有多组数据,所以每次清零t=k=0;//防止影响下一组数据ok=false;cin>>a[1];if(a[1]==0)break;for(int i=2;i<=n;i++)scanf("%d", &a[i]);for(int i=1;i<=n;i++){while(k==0||k!=0&&p[k]!=a[i]&&t<n)p[++k]=++t;if(p[k]!=a[i]){cout<<"No"<<endl;goto abc;//跳转到abc位置}else k--;}cout<<"Yes"<<endl;//注意大小写}cout<<endl;//每组数据换行}return 0;
}

例题:排队

今年有一堆人排队枪毙。每个人想知道站自己前面的、比自己高的人中,离自己近的是谁。
例:
排队者身高为[3,1,2,5,4,7,6]
则答案为[0,1,1,0,4,0,6]

维护一个栈,保存“观测者现在能看到的人”。
从左到右扫描队列。对每个元素x 执行如下操作:
1 弹栈,直到栈尾比自己高。
2 将栈尾作为自己的答案。若栈空,则自己的答案为0.
来模拟一波样例:[3,1,2,5,4,7,6]

单调栈

由于上文中的栈永远单调递减,故称为“单调栈”。
复杂度O(n),因为每个元素顶多入栈一次、出栈一次。
模板题

#include<bits/stdc++.h>
#define maxn 3000010
using namespace std;
int n,z[maxn],a[maxn],b[maxn],tot,ans[maxn];
//z表示n个元素的值,a表示第一个栈是栈的元素的值,b表示第二个栈对应a的第i个值的位置
int main()
{scanf("%d",&n);for(int i=1;i<=n;i++)scanf("%d",&z[i]);for(int i=n;i>=1;i--){while(tot>0&&a[tot]<=z[i])tot--;//维护单调栈,栈顶元素要大于现在加入的值,栈元素个数大于零ans[i]=b[tot];//更新答案tot++;a[tot]=z[i];b[tot]=i;}for(int i=1;i<=n;i++)printf("%d ",ans[i]);printf("\n");return 0;
}

队列

今有一群人排队枪毙,每次加入人都是在队尾,而每次取出人都是在队首。
收银台的队列、食堂恰饭的队列……无不如此。我们把这种队伍 抽象为数据结构“队列”。
队列是符合先入先出的结构。越早进入队列的,越早排完队出去。 对于所有的a,b 均有:若a 比b 早进入队列,则a 比b 早离开 队列。
队列提供如下操作: 查询队首、弹出队首、向尾部压入元素
代码实现
搞一个数组来保存元素。用fnt, end 分别记录头指针、尾指针。

int q[10005];
int fnt=1,end;
#define front (q[fnt])
#define pop   (fnt++)
#define push(x) (q[++end]=(x))

但是,q[fnt] 之前的所有空间都被浪费了!

#include<bits/stdc++.h>
#define maxn 3000010
using namespace std;
int q[maxn],l=1,r;
int main()
{cout<<q[l];//查询队首元素l++;//让队首出站q[++r]=x;//在队尾加入一个xreturn 0;
}

循环队列是指:在end 超出数组大小之后,把数据溢出到数组的头部

#define SIZE 100000;
int q[SIZE+5];
int fnt=1,end;

注意要保证SIZE一定比队列最长长度大!
STL 提供了队列,需要包含queue 库。
#include<queue>//c++
用法与stack 类似,但队首用front() 访问。

#include<queue>
using namespace std;
void caic()
{queue<int> s;//定义一个队列,存储int型for(int i=1;i<=5;i++)s.push(i);//压入队列while(!s.empty())//当队列非空时{printf("%d\n",s.front())//队首s.pop();//弹出}
}

显然,队列的三个操作——front, push, pop 时间复杂度全都是 O(1) 的。
STL 队列为动态空间。手写朴素队列空间会浪费大量空间(不停 地入队、出队);循环队列节省空间,但如果空间估算不足,会造成数据错误。

例题:约瑟夫问题

原题链接
起一个队列,初始为[1,2,3…n].
每轮循环:
1 队首出队,然后重新入队。执行m-1 次。 2 队首出队输出。
直到队列空为止。

#include<bits/stdc++.h>
#define maxn 1000010
using namespace std;
int d[maxn],l=1;r,n,m;
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)d[++r]=i;for(int i=1;i<=n;i++){if(m!=1)for(int j=1;j<m-1;j++){d[++r]=d[l];l++;}if(i!=n)printf("%d ",d[i]);else printf("%d\n",d[l]);l++;}return 0;
}

例题:求细胞数量

原题链接
扫描每一个点。我们可以通过如下方式走遍所在细胞中的每一个点:
先把这个点加入队列。当队列非空:
1.取出队首
2.标记队首四周没有走过的的细胞点,并把它们全都加进队列
队列空了之后,我们就标记完了这个细胞中所有的点。
上述手段称为bfs(宽度优先搜索)

单调队列

单调队列,即单调递减或单调递增的队列。使用频率不高,但在有些程序中会有非同寻常的作用。
模板题

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
int all, k;
struct node { int n, m; }a[maxn];
deque<node> q;
void BIG(int all, int k) {for (int i = 1; i <= all; ++i) {while (!q.empty() && a[i].m >= q.front().m )q.pop_front(); // 求最小的q.push_front(a[i]);while (q.back().n <= i - k) q.pop_back();if (i >= k) printf("%d ", q.back().m);}q.clear();
}
void LOW(int all, int k) {for (int i = 1; i <= all; ++i) {while (!q.empty() && a[i].m <= q.front().m) q.pop_front(); // 求最大的q.push_front(a[i]);while (q.back().n <= i - k) q.pop_back();if (i >= k) printf("%d ", q.back().m);}q.clear();
}
int main() {scanf("%d %d", &all, &k);for (int i = 1; i <= all; ++i) scanf("%d", &a[i].m), a[i].n = i;LOW(all, k);puts("");BIG(all, k);return 0;
}

并查集

刚刚实现的数据结构就是并查集(union-find)。并查集是用来维护不相交集合的数据结构,支持两个操作:
ask(x, y) 查询x,y 是否在同一个集合。
union(x, y) 将x,y 所在的集合合并。
带路径压缩的并查集,单次操作平均复杂度接近 O(1)

例题:家族问题

相关链接
今有n 个人,初始时互相没有关系。要支持多次操作:
1,查询x,y 是不是亲戚。
2,让x,y互相成为亲戚
一开始把每一个人的祖先设置为0
查询操作:
查询x,y 是不是亲戚时,找到x,y 各自的祖先。 如果相同,则返回true;如果不同,则返回false.
认亲操作:
如果x,y 已经在同一家族,则忽略。否则,将x 的祖先设为y

int anc(int x)//找到x的祖先
{if(dad[x])return anc(dad[x]);return x;
}
void ask(int x,int y)//查询是否为亲戚
{return anc(x)==anc(y);
}
void uni(int x,int y)//连接想,y
{x=anc(x),y=anc(y);if(x!=y) dad[x]=y;
}

容易发现,复杂度的瓶颈在于“找到某人的祖先”。最坏情况下, 1 是2 的父亲,2 是3 的父亲……子子孙孙无穷匮也,最后每次 找祖先要付出O(n) 的复杂度。
有办法加速吗?提示:我们自始至终只关心“x 的祖先是谁”,不 关心“x 的父亲是谁”。

路径压缩优化:确定了x 的祖先之后,直接把dad[x] 设为他的 祖先。正确性显然。

int anc(int x)//找到x的祖先
{if(dad[x])return dad[x]=anc(dad[x]);return x;
}
#include <bits/stdc++.h>
#define maxn 20010
using namespace std;
int fa[maxn],n,m,q,a1,a2;
int find(int p){if(fa[p])return fa[p]=find(fa[p]);return 0;
}
int main()
{scanf("%d%d%d",&n,&m,&q);for(int i=1;i<=m;i++){scanf("%d%d",&a1,&a2);int f1=find(a1),f2=find(a2);if(f1!=f2)fa[f2]=f1;}for(int i=1;i<=q;i++){scanf("%d%d",&a1,&a2);int f1=find(a1),f2=find(a2);if(f1==f2)printf("Yes\n");else peintf("No\n");}return 0;
}

模板题

模板题连接

例题:修复公路

原题链接

2021.1.29

某大学ACM实验室寒假新生培训Day7:动态规划入门

01背包

问题描述:有N种物品和一个容量是V的背包。每件物品只能使用一次。
第i件物品的体积是v[i],价值是w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

先从数学的角度抽象理解:
设f(i,j)为只考虑前i个物品的前提下,每个物品只能拿一次,最多用j容量能获得的最大价值。
显然,f(0,0),f(0,1),f(0,2)…f(0,m)的值为0。
假设已知f(i-1,0),f(i-1,1)…f(i-1,V),考虑如何求出f(i,j)(0≤j≤V)。
f(i,j)和f(i-1,j)的区别是f(i,j)还可以考虑第i个物品。
也就是说,f(i,j)只会根据取与不取第i个物品从之前的状态转移过来:
①不拿第i个物品最优:f(i-1,j)
②拿第i个物品下最优:f(i-1,j-w[i])+v[i](需要保证j>=w[i])
综上所述,f(i,j)=max(f(i-1,j-w[i])+v[i],f(i-1,j))
那么,以f(0,0),f(0,1),f(0,2)…f(0,V)为基础,我们可以依次求出f(i,j)(1≤i≤N,0≤j≤V)。
f(N,V)就是我们要求的答案。

考虑用一个二维数组f记录f(i,j)
代码如下:
时间复杂度O(N×V),空间复杂度O(N×V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[1005];int w[1005];
int f[1005][1005];
int main()
{cin>>N>>V;for(int i=1;i<=N;++i)cin>>v[i]>>w[i];for(int i=1;i<=N;++i){for(int j=0;j<=V;++j){f[i][j]=f[i-1][j];if(j>=v[i])f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);}}cout<<f[N][V]<<endl;return 0;
}

状态表示:
f[i][j]表示只考虑前i个物品的前提下,每个物品只能拿一次,最多用j容量能获得的最大价值。

状态转移方程:
f[i][j]=max( f[i-1][j],f[i-1][j-w[i]]+v[i] )

完全背包

问题描述:
有N种物品和一个容量是V的背包。每种物品都有无限件可用。
第i件物品的体积是v[i],价值是w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
回顾01背包的状态表示和状态转移方程,考虑完全背包。

01背包:
状态表示:
f[i][j]表示只考虑前i个物品的前提下,每个物品只能拿一次,最多用j容量能获得的最大价值。状态转移方程:
f[i][j]=max( f[i-1][j],f[i-1][j-w[i]]+v[i] )(j>=w[i])
完全背包:
状态表示:
f[i][j]表示只考虑前i个物品的前提下,每个物品能拿任意次,最多用j容量能获得的最大价值。状态转移方程:
f[i][j]=max( f[i-1][j],f[i][j-w[i]]+v[i] )(j>=w[i])
把i-1改成i就可以多次使用第i个物品了。

举个例子:
状态表示:
f[i][j]表示只考虑前i个物品的前提下,每个物品能拿任意次,最多用j容量能获得的最大价值。
N=2(物品数),V=5(背包容量)

状态转移方程:
f[i][j]=max( f[i-1][j],f[i][j-w[i]]+v[i] )(j>=w[i])f[0][1],f[0][2],f[0][3],f[0][4],f[0][5]=0
f[1][0]=0,S=∅
f[1][1]=max(f[0][1],f[1][0]+1)=1,S={1}
f[1][2]=max(f[0][2],f[1][1]+1)=2,S={1,1}
f[1][3]=max(f[0][3],f[1][2]+1)=3,S={1,1,1}
f[1][4]=max(f[0][4],f[1][3]+1)=4,S={1,1,1,1}
f[1][5]=max(f[0][5],f[1][4]+1)=5,S={1,1,1,1,1}
f[2][0]=f[1][0]=0,S=∅
f[2][1]=f[1][1]=1,S={1}
f[2][2]=max(f[1][2],f[2][0]+3)=3,S={2}
f[2][3]=max(f[1][3],f[2][1]+3)=4,S={1,2}
f[2][4]=max(f[1][4],f[2][2]+3)=6,S={2,2}
f[2][5]=max(f[1][5],f[2][3]+3)=7,S={1,2,2}

考虑用一个二维数组记录f(i,j)
代码如下:
时间复杂度O(N×V),空间复杂度O(N×V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[1005];int w[1005];
int f[1005][1005];
int main()
{cin>>N>>V;for(int i=1;i<=N;++i)cin>>v[i]>>w[i];for(int i=1;i<=N;++i){for(int j=0;j<=V;++j){f[i][j]=f[i-1][j];if(j>=v[i])f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);}}cout<<f[N][V]<<endl;return 0;
}

状态表示:
f[i][j]表示只考虑前i个物品的前提下,每个物品能拿任意次,最多用j容量能获得的最大价值。

状态转移方程:
f[i][j]=max( f[i-1][j],f[i][j-w[i]]+v[i] )

优化

01背包优化
时间复杂度O(N×V),空间复杂度O(V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[1005];int w[1005];
int f[1005][1005];
int main()
{cin>>N>>V;for(int i=1;i<=N;++i)cin>>v[i]>>w[i];for(int i=1;i<=N;++i){for(int j=V;j>=v[i];--j){f[j]=max(f[j],f[j-v[i]+w[i]]);}}cout<<[V]<<endl;return 0;
}

完全背包:
时间复杂度O(N×V),空间复杂度O(V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[1005];int w[1005];
int f[1005][1005];
int main()
{cin>>N>>V;for(int i=1;i<=N;++i)cin>>v[i]>>w[i];for(int i=1;i<=N;++i){for(int j=v[i];j<=V;++j){f[j]=max(f[j],f[j-v[i]+w[i]]);}}cout<<[V]<<endl;return 0;
}

多重背包

问题描述:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

solution1:

对于每种物品i,将它的si件单独拆开考虑,那么就变成了01背包。时间复杂度为O(Σ(s[i])×V)

#include<iostream>
#include<algorithm>
using namespace std;
int N;int V;
int v[105];int w[105];;int s[105];
int f[105];
int main()
{cin>>N>>V;for(int i=1;i<=N;++i)cin>>v[i]>>w[i]>>s[i];for(int i=1;i<=N;++i){for(int ith=1;ith<=s[i];++ith){for(int j=V;j>=v[i];--j){f[j]=max(f[j],f[j-v[i]+w[i]]);}}}cout<<[V]<<endl;return 0;
}

一个个拆开太笨,如果对于一种物品s[i]为一亿,那我们就要拆开成一亿份,DP一亿次,成本太大。
是否有更好的拆法,使得拆出来的份数少一点?且它们可以且仅可以组合成0~s[i]间的任意数呢?
可以用二进制拆分的方法,依次按照1,2,4,8,…来拆分,直到不足时剩下的来当最后一份。

举个例子:
拆分8(1000b):
第一次拆一份大小为20的,剩余7(111b)
第二次拆一份大小为21的,剩余5(101b)
第三次拆一份大小为22的,剩余1(1b)
第四次由于1<23而结束算法,直接将剩下的1个为1份。

证明二进制拆法的正确性:
回顾问题:是否有更好的拆法,使得拆出来的份数少一点?且它们可以且仅可以组合成0~s[i]间的任意数呢?
根据算法可知,我们最终拆分出来的大小分别为20,21,…2k,x。
证明仅可以:20+21+…+2k+x=s[i],不管怎么组合都不会超出s[i],能组合出来的数必然在0~s[i]之间。
证明可以:
①我们可以知道x<2k+1,也就是说x≤20+21+…+2k。
②证明可以组成0~x的任意数:
由二进制的性质,我们可知20,21,…2k可以组合成0~20+21+…+2k任意数。
又因为x≤20+21+…+2k。所以我们可以组成0~x任意数。
③证明可以组成x+1~s[i]的任意数:
先固定一个x,也就是说我们只需要用20,21,…2k组成1~s[i]-x的任意数。
因为20+21+…+2k=s[i]-x且20,21,…2k可以组合成0~20+21+…+2k任意数。
所以我们可以组成x+1~s[i]的任意数

solution2:

按照二进制优化的方法拆分。
时间复杂度为O(Σ(log2(s[i]))×V)

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int N;int V;
int v[1005];int w[1005];;int s[1005];
int f[2005];
int main()
{cin>>N>>V;for(int i=1;i<=N;++i)cin>>v[i]>>w[i]>>s[i];for(int i=1;i<=N;++i){for(int num=1;s[i];num<<=1){num=min(num,s[i]);for(int j=V;j>=num*v[i];--j){f[j]=max(f[j],f[j-num*v[i]+num*w[i]]);}s[i]-=num;}}cout<<[V]<<endl;return 0;
}

2021.1.30-2021.1.31

某大学ACM实验室寒假新生培训Day6:数据结构入门(补充)

堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵完全二叉树的数组对象。

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

二叉树

二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。
二叉树特点是每个结点最多只能有两棵子树,且有左右之分 。
数组储存二叉树
将1设置为树的根
节点标号i的父亲标号为i/2(向下取整)
节点标号i的左儿子标号为i×2
节点标号i的右儿子标号为i×2+1

堆的操作

push:往堆中加入一个元素;
top:从堆中取出堆顶元素;
pop:从堆中删除堆顶元素;

push

(1)在堆尾加入一个元素,并把这个结点置为当前结点。
(2)比较当前结点和它父结点的大小,
如果当前结点小于父结点,则交换它们的值,并把父结点置为当前结点。转(2)。
如果当前结点大于等于父结点,则转(3)。
(3)结束。

top

直接查询即可

pop

(1)取出堆的根结点的值。
(2)把堆的最后一个结点(len)放到根的位置上,把根覆盖掉。把堆的长度减一。
(3)把根结点置为当前父结点pa。
(4)如果pa无儿子(pa>len/2),则转(6);否则,把pa的两(或一) 个儿子中值最小的那个置为当前的子结点son。
(5)比较pa与son的值,
如果pa的值小于或等于son,则转(6);
否则,交换这两个结点的值,把pa指向son,转(4)。
(6)结束。

模板和代码

网上有很多关于堆的模板,由于STL中有priority_queue
模板链接
在信息学一系列竞赛中,由于堆比较难打(其实也没多难),一般都用pririty_queue来代替,但这并不代表学习关于堆的基础知识没用!

2021.1.25-2021.1.31相关推荐

  1. 【Doris Weekly FAQ】2021.07.05~2021.07.18

    观众朋友们: 晚上好! 欢迎收看[ Doris 近日要闻]~本次为您带来的是 2021年07月15日 - 2021年07月18日 的双周总结. Doris 社区周报每期会包含 FAQ 环节.我们会在社 ...

  2. 基于python的科技论文_实地科研 | 上海财经大学 | 金融科技、商业分析、人工智能:机器学习、人工智能及其在金融科技中的应用(2021.1.25开课)...

      课题名称   = 机器学习.人工智能及其在金融科技中的应用 =  项目背景   随着云时代的到来,机器学习.人工智能.大数据技术具有越来越重要的战略意义,并逐渐渗透到每一个行业和业务职能领域,成为 ...

  3. 读论文——Pre-Training with Whole Word Masking for Chinese BERT(2021 11.25)

    第一遍 标题以及作者(2021 11.25) 摘要 本文基于BERT,在RoBERTa上进行一系列改进,提出了用于中文的预训练模型MacBERT. 提出了一种新的掩码策略,MLM as correct ...

  4. Diabetes 糖尿病及其并发症.|2021/1/25(未完待续)

    目录 前言: 正文: ①宏观严重性: ②普遍状况:" 三多一少": ③定义 : ③胰岛素(Insulin,一种激素(harmone)) ④引出糖尿病病理: 1> 1-型糖尿病 ...

  5. 【2021.12.25】ctf逆向中常见加密算法和编码识别

    [2021.12.25]ctf逆向中常见加密算法和编码识别(含exe及wp) 文章目录 [2021.12.25]ctf逆向中常见加密算法和编码识别(含exe及wp) 0.前言 1.基础加密手法 2.b ...

  6. vue+element 实现试卷答题功能,单选题 ,多选题,判断题,简答题(2.0版本,2021.3.25更新)

    vue+element 实现 试卷答题功能,单选题 ,多选题,判断题,简答题(2.0版本,2021.3.25更新) 文章目录 vue+element 实现 试卷答题功能,单选题 ,多选题,判断题,简答 ...

  7. 2021.2.25课程摘要(逻辑教育-王劲胜)

    2021.2.25课程摘要 逻辑教育-13期-Python基础班-王劲胜 一.面向对象(中) 二.面向对象(下) 逻辑教育-13期-Python基础班-王劲胜 一.面向对象(中) ☆property装 ...

  8. 公众号内容拓展学习笔记(2021.3.25)

    公众号内容拓展学习笔记(2021.3.25)

  9. 2021.1.25写写日记

    类 类是抽象的 类实例化以后会返回一个自己的对象 public class Student{//创建一个学生类//属性:字段string name;int age;string brith; } pu ...

  10. python自动化处理,获得免费wps会员,云函数2021.4.25反馈失效(以后不进行此文章维护了)

    2021.2.18 2021.2.20更新: 现在邀请过快会被过滤,一秒钟邀请10个人只算一个,要手动添加time.sleep(10)每次邀请延时十秒,即可继续使用. 2021.2.21更新: 更新云 ...

最新文章

  1. 联邦学习应用思考:需求还是方法?
  2. SpringBootH ttpInvoker接口调用
  3. ASP.NET Core Web Razor Pages系列教程一:使用ASP.NET Core 创建一个Razor Pages网络应用程序
  4. 软件测试用例设计实用经验之谈
  5. 适用于芯片验证工程师的atom插件列表
  6. 第三次作业-结对编程
  7. string修饰的梦修改吗_Java String 对象,你真的了解了吗?
  8. [JavaScript] 设置函数同名变量为false会导致函数无法执行
  9. socket 选项 详细说明
  10. java描述常用的集合类_Java常用的集合类
  11. 自学python免费教材-最好的Python入门教材是哪本?
  12. python异常类父类_python【第五篇】异常处理
  13. linux limbo镜像文件下载,limbo linux镜像下载
  14. 布控球可接入电网安全接入平台及电网统一视频
  15. 系统托盘安全删除硬件图标不见了(任务栏USB图标不见了)的故障处理图文详解
  16. IDEA Schemas and DTDs
  17. 学计算机提升,【思想提升】学计算机,就是做计算机的吗?
  18. 为什么要用企业邮箱?企业邮箱能给公司带来哪些好处?
  19. arduino简易呼吸灯实验
  20. 连连看c语言代码,连连看C语言代码.doc

热门文章

  1. 时间戳与日期年月日时分秒的转换
  2. 搜索引擎优化技巧如何看待
  3. 证明spring中property name=这个双引号的内容只与setter方法有关,与一个类定义的字段和getter方法无关...
  4. 【学习机器学习】实验——聚类算法性能度量
  5. easy-captcha实现验证码功能
  6. web前端入门到实战:纯HTML做出几个实用网页效果
  7. 计算机维修中拆机工具有哪些,秒变专业拆机维修达人,这款工具套装你值得拥有...
  8. Python爱好者 socket模块传输文件 -
  9. [设计模式] - 代理模式(静态代理与动态代理)
  10. SAP HANA命令行方式备份恢复