暑假算法集训(第三周)
进入算法集训的第三周了,已经过去了大半个月,也学习到了很多新的知识点。这周开始就学很多使代码更有效率的算法,如双指针,前缀和,差分。这些知识点都比较基础,但在运用到题目中往往没那么简单。但熟练都是靠刷题练出来的,即使后面开学也要有自我刷题的习惯,总结题目,理解别人的优质代码。加油!
知识点回顾
贪心法
概念分析
贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
找出全局最优解的要求
在遇见问题时如何确定是否可以使用贪心算法解决问题,那么决定一个贪心算法是否能找到全局最优解的条件。
- 最优子结构(optimal subproblem structure,和动态规划中的是一个概念)
- 最优贪心选择属性(optimal greedy choice property)
例题分析
双指针
概念分析
双指针技巧可细分分为两类,一类是快慢指针,一类是左右指针。
前者解决主要解决链表中的问题,比如典型的判定链表中是否包含环、反转链表、找链表的中间节点、删除链表的倒数第 N 个结点;也用来解决数组中的问题,如移动/移除元素、删除有序数组中的重复项。
后者主要解决数组(或者字符串)中的问题,比如二分查找,滑动窗口。
(图示分析)
前者为在两个序列中同时分别进行指针,后者为在同一序列中前后进行操作的指针。
作用分析
由于具有某种单调性,朴素算法往往能优化为双指针算法。
区别:
朴素算法每次在第二层遍历的时候,是会从新开始(j会回溯到初始位置),然后再遍历下去。(假设i是终点,j是起点)
双指针算法:由于具有某种单调性,每次在第二层遍历的时候,不需要回溯到初始位置(单调性),而是在满足要求的位置继续走下去或者更新掉。
优化代码效率,减少不必要的重复。
例题分析
1.纪念品分组
前缀和与差分
前缀和是指某序列的前n项和,可以把它理解为数学上的数列的前n项和,而差分可以看成前缀和的逆运算。合理的使用前缀和与差分,可以将某些复杂的问题简单化。解题时一般为一维或者二维。
主要作用:
数据预处理,用于降低查询时的时间复杂度。
差分:类似于数学中的求导和积分,差分可以看成前缀和的逆运算。
差分数组:
首先给定一个原数a:a[1], a[2], a[3],,,,,, a[n];
然后我们构造一个数组b : b[1], b[2], b[3],,,,,, b[i];
使得 a[i] = b[1] + b[2] + b[3] + ,,,,,, + b[i]
也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。换句话说,每一个a[i]都是b数组中从头开始的一段区间和。
例题回顾
1.P3406 海底高铁
2.P2879 [USACO07JAN] Tallest Cow S
整数二分和实数二分
概念分析
非线性方程是指 f (x)中含有三角函数、指数函数或其他超越函数(如对数函数,反三角函数,指数函数等)。这些方程很难求得精确解,不过在实际应用中,只要满足一定精度要求的近似解就可以了。此时需要考虑以下两个问题:
- 根的存在性。定理:函数 f (x) 在闭区间 [a,b] 上连续,且 f (a) · f (b) < 0 ,则 f(x)存在根。
- 求根。一般有两种方法,搜索法和二分法,这边只介绍二分法。
二分法:如果确定 f (x) 在闭区间 [a,b] 上连续,且 f (a) · f (b) < 0(也就是根存在的情况下),把 [a,b] 逐次分半,检查每次分半后区间两端点函数值符号的变化,确定有根的区间。
算法竞赛中有两种二分题型:整数二分和实数二分。整数二分要注意边界问题,避免漏掉答案或者陷入死循环;实数二分要注意精度问题。
例题分析
例题回顾
p3406 海底高铁
题目分析
这道题是指一个人要去不同城市访问,不同城市间有买票或者买ic卡支付。这道题一开始觉得就是简单的前缀和呀,把每个城市到钱为车票或者买ic卡的两个的最小值,并且只要计算每次形成的开始和结束就行,当我信心满满的运行案例时比答案多了2000多,主要我自己手算算不到这个答案,后来就红温了!后来多次研读题目后发现,其实每一段的ic卡,如果有重复就只要买一次就行,可以提前充值,恍然大悟,在通过差分记录每个站的起始的差分,在通过计算买ic卡和买车票那个划算确定,这一段买不买ic卡。
(一开始的错误代码)
#include<bits/stdc++.h>
#include<cmath>
using namespace std;
#define int long long
const int maxn = 1e6;
int num[maxn]={0};
int a[maxn]={0};
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int n,m;cin>>n>>m;
for(int i=1;i<=m;i++)cin>>a[i];
for(int i=2;i<=n;i++){
int a,b,c;cin>>a>>b>>c;
num[i]=num[i-1]+min(a,b+c);//记录下每个前缀和
}
int ans=0,sp1=0;
for(int i=2;i<=m;i++){
int start=min(a[i],a[i-1]);
int end = max(a[i],a[i-1]);
sp1=(num[end]-num[start]);
ans+=sp1;
}
cout<<ans<<endl;
return 0;
}
(ac代码)
#include<bits/stdc++.h>
using namespace std;
int n,m,p,c[100005],p2,p1,a,b,c1;
long long sum,ans;
int main()
{
cin>>n>>m;
if(m>0)cin>>p1;
for(int i=2;i<=m;i++)
{
cin>>p2;
if(p1<p2)c[p1]++,c[p2]--;//将其实位置加一,结束位置减一。
else c[p2]++,c[p1]--;
p1=p2;
}
for(int i=1;i<n;i++)
{
sum+=c[i];
cin>>a>>b>>c1;
if(sum!=0)ans+=min(a*sum,b*sum+c1);//判断选择那种方式
}
if(m<=1)ans=0;
cout<<ans;
return 0;
}
P2879 [USACO07JAN] Tallest Cow S
题目分析
这道题是差分的变形,可以判断是否掌握了差分。题意是说有许多牛,会告诉你最高的牛的位置和高度,然后就会给你一系列的牛的区间,意思是两段的牛可以一样高,中间的一定比他们小。最后输出每个位置牛最高的可能。利用差分性质把区间的就的差记录下来,在最后体现在高度上。
(ac代码)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e4+5;
bool num[maxn][maxn]={false};
int num1[maxn]={0};
int num2[maxn]={0};
int main(){
int n,i,h,r;cin>>n>>i>>h>>r;
while(r--){
int a,b;cin>>a>>b;
if(a>b)swap(a,b);
if(!num[a][b]){
num1[a+1]--;
num1[b]++;}
num[a][b]=true;
}
for (int i=1;i<=n;i++)
{
num2[i]=num2[i-1]+num1[i];
printf("%d\n",h+num2[i]);
}
return 0;
}
P2695 骑士的工作
题目分析
这道题其实很简单就是普通的贪心算法,那什么要记录他,是因为这道题是我最早看到的算法习题书的的第一道题当时觉得很高兴的题目现在看来也是手拿把掐,但仍要不断积累。这道题其实就是将龙的头但值得的大小一次排列,一个其实看点一个他能砍的最小值的龙,如果最后又头没砍完则输出-1。
(ac代码)
#include<bits/stdc++.h>
using namespace std;
int num[1000000],num2[1000000];
bool cmp(int e1,int e2){
return e1<e2;
}
int main(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++){
int x;cin>>x;
num[i]=x;
}
for(int i=1;i<=m;i++){
int x;cin>>x;
num2[i]=x;
}
sort(num+1,num+1+n,cmp);
sort(num2+1,num2+1+m,cmp);
int sum=0,count=0;
if(n>m){
cout<<"you died!";
return 0;
}
for(int i=1;i<=n;i++){
int h=num[i];
for(int j=1;j<=m;j++){
if(num2[j]>=h){
sum+=num2[j];
num2[j]=0;
count++;
break;
}
}
}
if(count==n){
cout<<sum;
}else{
cout<<"you died!";
}
return 0;
}
(ac代码)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+100;
int num[maxn];
int main(){
int w;cin>>w;
int n;cin>>n;
for(int i=1;i<=n;i++){
cin>>num[i];
}
sort(num+1,num+1+n);
int ans=0;
int i=1,j=n;
while(i<=j){
if(num[i]+num[j]>w){
if(num[i]>w){ans+=j-i+1;break;}
else{
ans++;
j--;
}
}
else{
ans++;
i++;j--;
}
}
cout<<ans<<endl;
return 0;
}
题目分析
这道题是要求一个数的立方根,当让你可以使用库函数cbrt()或者pow(n,1.0/3)但这道题也可以使用二分来解题。首先确定值得范围,因为数最大到10^15,所以立方根后为10的5次方。那个在1和10的5方内进行二分,如果大那么将右边界缩小到中值的左边,如果小那就左值放大到中值的右边。最后当左值大于右值结束。因为要向下取整,所以选择小的右值输出。
(ac代码)
#include <bits/stdc++.h>
using namespace std;
long long f(long long a) {
return a * a * a;
}
int main() {
long long a;cin >> a;
long long i = 1, j = 1e5+100;
long long ans = 0, mid;
while (i <= j) {
mid = (i + j) / 2;
if (f(mid) > a) {
j = mid - 1;
} else if (f(mid) < a) {
i = mid + 1;
} else {
break;
}
}
if(f(mid)==a)cout<<mid;
else cout<<j;
return 0;
}