文章目录
ZZU ACM培训 基础算法 笔记2
模拟
取模遍历
从中间某数b开始将数组遍历一周
for(int i=b;i<=n-1;i++){
cout<<a[i]<<" ";
}
for(int i=0;i<b;i++){
cout<<a[i]<<" "
}
以上为取模遍历。
相比普通按顺序分两段遍历,取模遍历明显更为简便。
for(int i-0;i<n;i++){
cout<<a[(b+1)%n]<<" ";
枚举
百钱买百鸡典例
运用三层嵌套循环,并通过限制数量为整型,限制变量范围,从而逼近答案,减小时间复杂度.
减少枚举变量
- 解答中,除去最后一套循环。把for循环改成对小鸡数量的关系式表达,可以减少代码的复杂度,和时间复杂度
- 可以再进一步,利用方程组的形式,优化代码,减少至最后只有一个循环。
首尾相接凑2021
去掉z中间某段:
for(int i=0;i<n;i++){ //去掉的起点
for(int j=i;j<n;j++) //去掉的终点
}
/*s.substr(p,n) 返回从s的下标p开始的n个字符组成的字符串,如果n省略就取到底O(n) little cpp stl */ string t=s.substr(0,i)+s.substr(j+1 )
bool ok=false; ...... if(t=="2021"){ ok=ture; } cout<<(ok?"yes":"no")<<"\n"; //一个用来判断的小技巧
代码优化:
// 初始代码
bool ok=false;
for(int i=0;i<n;i++){ //去掉的起点
for(int j=i;j<n;j){ //去掉的终点
string t=s.substr(0,i)+s.substr(j+1)
if(t=="2021"){
ok=true
}
}
//优化后代码
bool ok=false;
for(int i=0;i<=4;i++){
string t=s.substr(0,i)+s.substr(n-(4-i))
if(t=="2021"){
ok=true
}
}
校园活动(之后再编)
递推递归
常用于序列计算,序列中的每一项依赖于前面一个或多个项的值
递归更像是一种编程技巧,代码会更加简洁
一个函数调用自己即是递归
递归递推差异
- 二者对问题求解方式本质上是一样的,不同的是求解次序。
- 从前往后是递推,从后往前求是递归。
- 有已知到未知是递推,由未知到已知是递归。
阶乘
计算某数的阶乘
最简单的算法题.定义一个long long类型的数组
long long fac[n+1]{}; //long long fac[n+1]={};
然后直接递推,得出结果
递归解法如下:
long long fac(int n){
if(n==0){
return 1;
}
return n*fac(n-1);
}
int main(void){
int n;
cin<<n;
return 0;
}
//优化一下
long long fac(int n){
return n==0?1:n*fac(n-1);
}
兔子兔子
斐波那契数列
F n = F n − 1 + F n − 2 , n > = 2 F_n=F_{n-1} + F_ {n-2},n>=2 Fn=Fn−1+Fn−2,n>=2
// 递推写法
int main(void){
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;
}
//递归写法
long long f(int n){ //返回第n月有多少兔子
if(n==1orn==2){
return 1;
}
return f(n-1)+f(n-2);
}
int main(void){
int n;
cin>>n;
cout<<f(n)<<"\n";
return 0;
}
碎梦
题干:窗台上有 n堆纸飞机数目分别为 1 , 2 , … , n 。每次可以选择一个数 x,然后从所有纸飞机数目大于等于x的堆中掷出x只纸飞机.问最少要选择多少次才能将所有纸飞机掷出?
递归最重要的是终止条件和递归函数体
//取中值开涮
/*
n=5
1 2 3 4 5
x=1; 0 1 2 3 4
x=2; 1 0 1 2 3
x=3; 1 2 0 1 2
x=4; 1 2 3 0 1
x=5; 1 2 3 4 0
n=6
1 2 3 4 5 6
x=1; 0 1 2 3 4 5
x=2; 1 0 1 2 3 4
x=3; 1 2 0 1 2 3
x=4; 1 2 3 0 1 2
x=5; 1 2 3 4 0 1
x=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);//(n-1)/2==n/2
}else{
r#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);//(n-1)/2==n/2
}else{
return 1+solve(n/2);
}
}
int main(void){
int n;
cin>>n;
cout<<solve(n)<<"\n";
return 0;
}
//优化
if(n==1){ //solve(n)=1+solve(n/2)
//f[n]=1+f[n/2] 递推代码
return 1;
}return 1+solve(n/2);
//再优化
return n==1?1:1+solve(n/2);
(n-1)/2与n/2
在n为奇数条件下,两者相同;若为偶,则不相同
二分
引子
给n个数,m次询问,每次询问给一个数a,找到n个数中比a小的最大的数,数据保证这样的数存在。
把n个数排序,把此数组依照从小到大的顺序遍历,找到小的最大的数。此种算法每次询问时遍历一遍原数组,时间复杂度 O(n*m).
离线
把n次询问先进行操作,即先把数组排序,查询值也排序,遍历一遍,可一一对应。 时间复杂度O(nlog n)
STL
//运用set
set<int>s;
s.lower_bound(a);
s.lower_bound(a--);
方法
我们先将数组进行排序,对于每次询问假设答案的下标在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的过程非常慢,是否有简便方法?
简单来说,我们维护一个区间使答案必定在这个区间内,然后我们继续维护,使得左端点必定合法(check为1),右端点必定不合法(check为0)然后不断通过取左右端点的中点的方式来每次将这个区间缩短一半,这样当ls+1=rs的时候,左端点就是答案。
其实就是一个左闭右开的区间。
bool check(int id){ //贪心扫描
if(w[id]<a) return 1;
return 0;
}
void work1(){
int ls=1,rs=n+1; //左闭右开,保证左端点永远合法
while(ls+1<rs){
in mid=(ls+rs)/2;
if(!check(mid)) rs=mid;
else ls=mid;
}
printf("%d",w[ls]);
}
简单来说,如果我们已经实现了check函数,那么无非是下列两种情况
11111110000000 让你求最大的1的位置
00000001111111 让你求最小的1的位置
第一个就是上述方法(左闭右开)
第二个只需要维护ls永远非法,rs永远合法即可(左开右闭)。
二分的难点其实在于check函数的实现,即如何判断当前位置的数是否满足题目要求
NOIP2015 跳石头
(终于开始写题了!)
数轴上有n个石子,第i个石头的坐标为Di,现在要从0跳到L,每次跳都从一个石子跳到相邻的下一个石子。现在FJ允许你最多移走M个石子,问移走这M个石子后,相邻两个石子距离的最小值最大是多少。(N<=50000,L<=1e9)
/*10
1 2 3 4 5 6 7 8 9 10(to be honest 算法竞赛上我最大的石头反倒是阅读理解题目含义,而非算法,这就是思维流化的坏处吗) */
“最小值最大”二分算法经典标志
//移走M即为移走 两个相隔间的最小,使其在不断移走中不断变大
//如果a对应的M合法,a-1对应的M更小,因此必定合法,此a-1不是说两数间距离此时为a-1,而
//是当最小距离为a-1合法。如果把a-1理解为此时某两数间距离,则不合法,需要进行移动
#include<bits/stdc++.h>
using namespace std;
int d,n,m;
int a[maxn];
int l,r,mid,ans;
inline int read(){//我喜欢快读
int num = 0;
char c;
bool flag = false;
while ((c = getchar()) == ' ' || c == '\n' || c == '\r');
if (c == '-') flag = true;
else
num = c - '0';
while (isdigit(c = getchar()))
num = num * 10 + c - '0';
return (flag ? -1 : 1) * num;
}
bool judge(int x){//judge函数,x代表当前二分出来的答案
int tot = 0;//tot代表计数器,记录以当前答案需要移走的实际石头数
int i = 0;//i代表下一块石头的编号
int now = 0;//now代表模拟跳石头的人当前在什么位置
while (i < n+1){//千万注意不是n,n不是终点,n+1才是
i++;
if (a[i] - a[now] < x)//判断距离,看二者之间的距离算差值就好
tot++;//判定成功,把这块石头拿走,继续考虑下一块石头
else
now = i;//判定失败,这块石头不用拿走,我们就跳过去,再考虑下一块
}
if (tot > m)
return false;
else
return true;
}
int main(){
d = read();//d代表总长度,也就是右边界
n = read();//n块石头
m = read();//限制移走m块,思考的时候可别被这个m限制
for (int i=1;i<=n;i++)
a[i] = read();
a[n+1] = d;//敲黑板划重点,再强调一遍,n不是终点
l = 1;//l和r分别代表二分的左边界和右边界
r = d;
while (l <= r){//非递归式二分正常向写法,可理解为一般框架
mid = (l+r) / 2;//这再看不出是啥意思可以退群了
if (judge(mid)){//带入judge函数判断当前解是不是可行解
ans = mid;
l = mid + 1;//走到这里,看来是可行解,我们尝试看看是不是有更好的可行解
}
else
r = mid - 1;//噫,你找了个非法解,赶紧回到左半边看看有没有可行解
}
cout << ans << endl;//最后的ans绝对是最优解
return 0;
// 以上源代码来自洛谷ShawnZhou题解
}
-
如果一个距离满足题意,则更小的距离肯定满足,所以答案满足单调性,考虑二分答案。
-
那么我们在跳跃距离[0,L]之间二分枚举一个最小跳跃距离作为答案mid进行判断,看是否符合最多移走m个的限制,如果符合,那么mid可能就是答案,但也可能还存在更大的答案,所以要向右二分查找。如果不行,那肯定大了,向左二分查找。
-
判定直接O(N)的贪心即可
-
总时间复杂度为NlogL