基础算法学习笔记

贪心

2020.9.12将一本通上的贪心学的差不多了,现在就整理一下。

1.选择不相交区间问题

例题:P1803 凌乱的yyy / 线段覆盖

分析:板子题,没什么好说的,总之就是每次贪心选择活动时间尽量少并且与之前的活动不相交的,不多评述。

2.区间选点问题

例题:P1250 种树

分析:给定许多区间,叫你取尽量少的点达成每个区间都至少有一点的目标。解决方案就是每一次选择区间末尾的那个点,使得这个点能同时存在于尽量多的区间内。

3.区间覆盖问题

例题:#10002. 「一本通 1.1 例 3」喷水装置

分析:给你几个区间,让你选几个区间覆盖一个大的。解决方案就是先把区间按左端点大小进行排序,每次再贪心选择右端点最大的,直到将区间覆盖为止。

4.流水作业调度问题

例题:P1248 加工生产调度

分析:这种类型的题属比较难的,但我做了这么久的题也没见过几回,就这一道。有\(M_1\)\(M_2\)两台机器,要现在\(M_1\)上加工,再到\(M_2\)上加工。思路是要使用\(Johnson\)算法:让\(M_1\)没有空闲,让\(M_2\)空闲时间尽量短。

可以证明得到一个结论:\(N_1\)\(a<b\)的作业集合,\(N_2\)\(a\ge b\)的作业集合,将\(N_1\)的作业按\(a\)非减序排序,\(N_2\)中的作业按\(b\)非增序排序,就可达成最优顺序

因为感觉实在不会来考,所以不准备证明,想看代码实现和证明看书就行,不在这里多扯。

5.带限期和罚款的单位时间任务调度

例题:P1230 智力大冲浪

分析:将所有事件根据罚款大小排序,从罚款多的开始处理,再一个个看能不能安排在所需的时间段内,不能就放弃处理。

6.习题

简单的习题就不写了,这里只给出两道比较难的。

  1. P1717 钓鱼

  2. P2512 [HAOI2008]糖果传递

思路就不想再讲了,时间没那么多,有时间再自己去看题目。

总结

实际上,考场上纯考贪心的题实在不多,所以此处讲的这些价值大不大?着实是个问题。但是要知道的是,考场上与贪心结合起来考的题非常多,所以贪心的思想一定要掌握好,要把握住贪心的精髓。

二分

2020.10.12日时学习,刚好与贪心隔了一个月(笑)

二分查找就不说了,傻子都会。

重点:二分答案

二分答案怎么说呢,入门题应当是这一道P1182 数列分段 Section II,其实当时理解还很浅薄,虽然将题解看了几遍,但还是处于云里雾里的状态,懵懵懂懂的就过了,后面在看一本通时终于有一次豁然贯通,才理解了二分答案。

二分答案,顾名思义,我们可以根据题意确定答案的上下界,然后不断二分,确定一个答案,接着通过一个判断函数来判断这个答案是否符合要求,接着就是缩小范围,直到找到那个最优解。

适用题目类型:最大值最小或最小值最大,如果出现这两种字眼,那不用怀疑,妥妥的是二分答案了。

板子题挺多的,就不一一列举,但再在整型二分之外还有另一种二分类型,更难的实数二分,对于这种类型的题目,精度一定要格外注意,稍不留神便会被精度坑死。

例题:P3743 kotori的设备

想了想,还是放一个板子

while(r-l>=1e-6)
{
	double mid=(l+r)/2;
	if(judge(mid))l=mid;
	else r=mid;
}

嗯,精度要开大一点是比较保险的,不过建议也不要太大,1e-12就有点过分,一般1e-8是完全够用的。

冷门的三分法

先放一个模板传送门:P3382 【模板】三分法

如果说二分是专门针对具有单调性的问题的话,那么三分就一定是针对具有单峰性的问题。与三分有关的问题,其答案范围构成的函数一般都具有一个单峰或低谷,分别对应答案的最大值,最小值,模板如下。

while(r-l>=1e-6)
{
	double m1=l+(r-l)/3.0,m2=r-(r-l)/3.0;
	if(f(m1)<f(m2))l=m1;
	else r=m2;
}

f函数就要根据题意设定,意为答案取此时的函数值。

三分习题

  1. P5931 [清华集训2015]灯泡

简单说一下,其实也没什么好说,利用初中数学知识再套三分模板就可过,分类讨论很淦。

  1. P2571 [SCOI2010]传送带

没有做好心理准备不建议乱上。函数带两个变量就很淦,一定要先确定一个变量,需使用三分套三分的方法。另,这里的double多的让我快打吐。

总结

二分思想一样很重要,虽然其实在考试里出现的似乎不多,不过基础算法毕竟是基础,学好是很必要的。至于三分?有兴趣的可以学一学,没兴趣的让它爬就行,CCF要是出三分题就是脑子进水了。实数二分不用太担心,因为精度问题实在是太少出现在考场上。总之还是四个字:领悟思想

真正的重点:搜索

FIRST 深搜(dfs)

先放一个网上找来的模板

void dfs(答案,搜索层数,其他参数){
    if(层数==maxdeep){
        更新答案;
        return; 
    }
    (剪枝) 
    for(枚举下一层可能的状态){
        更新全局变量表示状态的变量;
        dfs(答案+新状态增加的价值,层数+1,其他参数);
        还原全局变量表示状态的变量;
    }
}

搜索,尤其是深搜,对于非常多题都适用,跟枚举的纯暴力相差无几,容易打,不易出错,很多题用来对拍的暴力都是这种。它的时间复杂度是指数级别的,但是有时题目的正解,可能就是深搜这种暴力,正常跑是肯定跑不过去,但若是能优化,结果或许就会大不一样。

说了一堆废话,回到正题。这里的主角是深搜,但是却是有剪枝的深搜,剪掉深搜的搜索树上的那些无用枝条,使得深搜能在极短的时间内找出答案,这便是我们的正解。

但却有三个加剪枝一定注意:正确性,准确性,高效性。做到这三点,那才能算真正优化到了。

优化技巧有很多,我现将书上的分类搬下来,以便观看。

1.优化搜索顺序 2.排除等效冗余 3.可行性剪枝 4.最优性剪枝 5.记忆化

先来看一道剪枝的入门题P1025 数的划分

这里使用搜索可以说是很明显了,要打出来也很简单,但是分析一下复杂度就知道,TLE是稳稳的。所以,这里一定要加剪枝。如何剪枝呢?其实也很简单,只要分析一下上下界即可,因为不考虑顺序,所以可以设\(a[i-1]\le a[i]\),现在下界已经有了,也可以轻易推出上界是\(\frac{m}{k-i+1}\),只要加了这两个简单的剪枝,就可以愉快的AC了。

再来看一道剪枝的经典题目P1120 小木棍 [数据加强版]

这道题的剪枝异常之多,大类里可以分出最优性剪枝与可行性剪枝两类,根据题目具体细节就可以写出很多剪枝,是一道很好的剪枝练习题。

经典题目还是放一下代码

代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int a[105],n,len,m,minn,sum,bj,tot,b[105],next[105];
bool ex[105];
bool cmp(const int &x,const int &y)
{
	return x>y;
}
void read()
{
	cin>>n;
	int d;
	for(int i=1;i<=n;i++)
	{
		cin>>d;
		if(d>50)continue;
		a[++tot]=d;
		sum+=d;
	}
	sort(a+1,a+tot+1,cmp);
	next[tot]=tot;
	for(int i=tot-1;i>0;i--)
	{
        if(a[i]==a[i+1]) next[i]=next[i+1];
        else next[i]=i;
    }
}
void dfs(int k,int last,int rest)
{
	int i,j;
	if(rest==0)
	{
		if(k==m){bj=1;return;}
		for(i=1;i<=tot;i++)
		 if(!ex[i]){ex[i]=1;break;}
		dfs(k+1,i,len-a[i]);
		ex[i]=0;
		if(bj)return;
	}
	int l=last+1,r=tot,mid;
	while(l<r)
	{
		mid=l+r>>1;
		if(a[mid]<=rest)r=mid;
		else l=mid+1;
	}
	for(i=l;i<=tot;i++)
	{
		if(!ex[i])
		{
			ex[i]=1;
			dfs(k,i,rest-a[i]);
			ex[i]=0;
			if(bj)return;
			if(rest==a[i]||rest==len)return;
			i=next[i];
			if(i==tot)return;
		}
	}
}
void solve()
{
	int i,j;
	for(int i=a[1];i<=sum/2;i++)
	{
		if(sum%i==0)
		{
			len=i;
			ex[1]=1;
			bj=0;
			m=sum/i;
			dfs(1,1,len-a[1]);
			if(bj){cout<<len<<endl;return;}
		}
	}
	cout<<sum;
}
int main()
{
	read();
	solve();
	return 0;
}

习题

  1. P1074 靶形数独(比较好的剪枝练习题虽然可以被生草做法过掉

  2. P1283 平板涂色(写了题解,似乎比较水?)

  3. UVA12558 埃及分数(迭代加深搜索,这里好像没讲?有时间补上)

SECOND 广搜(bfs)

除了深搜之外,搜索的另一利器是广搜。打个形象的比方,如果说深搜是一条路走到黑,不撞南墙不回头的话,广搜就是对每种枝条都稍微一探,如果在这一层没有找到结果,再逐级深入。

同时因为广搜的特殊性,广搜搜到的第一个符合条件的答案往往就是最优解。

因为广搜是利用队列进行工作的,所以判重,也就是判断这个元素有没有在队列里,是一个很重要的工作。但是有些题的判重并没有那么简单,例如此题:P2730 [USACO3.2]魔板 Magic Squares。这里的元素是一个序列,我们需要对其进行状态压缩,给它每一种不同的序列都标一个号,在判重时使用这个序号来判重。

此题判重还需要学会康托展开,有兴趣的可以点击模板传送门自行学习,这里不会讲述。

魔板代码也放这

代码
#include<bits/stdc++.h>
using namespace std;
int jc[10]={1,1,2,6,24,120,720,5040};
int g,st,prt[50005],b[1000000]={0},step[50005];
char a[50005];
struct node{
	int a[2][4];
}start,goal,q[90000];
int turn(node x)
{
	int i,j,res=0,t[8],s;
	for(i=0;i<4;i++)t[i]=x.a[0][i];
	for(i=3;i>=0;i--)t[7-i]=x.a[1][i];
	for(int i=0;i<8;i++)
	{
		s=0;
		for(j=i+1;j<=7;j++)if(t[j]<t[i])s++;
		res+=jc[7-i]*s;
	}
	return res;
}
node change(int way,int num)
{
	node tep;
	if(way==1)
	{
		for(int i=0;i<4;i++)tep.a[0][i]=q[num].a[1][i];
		for(int i=0;i<4;i++)tep.a[1][i]=q[num].a[0][i];
		return tep;
	}
	if(way==2)
	{
		tep.a[0][0]=q[num].a[0][3];
		tep.a[1][0]=q[num].a[1][3];
		for(int i=1;i<4;i++)tep.a[0][i]=q[num].a[0][i-1];
		for(int i=1;i<4;i++)tep.a[1][i]=q[num].a[1][i-1];
		return tep;
	}
	if(way==3)
	{
		tep.a[0][0]=q[num].a[0][0];tep.a[1][0]=q[num].a[1][0];
		tep.a[0][1]=q[num].a[1][1];tep.a[1][2]=q[num].a[0][2];
		tep.a[0][3]=q[num].a[0][3];tep.a[1][3]=q[num].a[1][3];
		tep.a[0][2]=q[num].a[0][1];tep.a[1][1]=q[num].a[1][2];
		return tep;
	}
}
void print(int num)
{
	if(num==1)return;
	print(prt[num]);
	cout<<a[num];
}
void bfs()
{
	int op=1,cl=1,i,t;node tep;
	q[1]=start;step[1]=0;prt[1]=1;
	while(op<=cl)
	{
		for(int i=1;i<=3;i++)
		{
			tep=change(i,op);
			t=turn(tep);
			if(!b[t])
			{
				q[++cl]=tep;
				step[cl]=step[op]+1;
				b[t]=1;
				prt[cl]=op;
				a[cl]=char('A'+i-1);
				if(t==g)
				{
					cout<<step[cl]<<endl;
					print(cl);return;
				}
			}
		}
		op++;
	}
}
int main()
{
	for(int i=0;i<4;i++)start.a[0][i]=i+1;
	for(int i=3;i>=0;i--)start.a[1][i]=8-i;
	st=turn(start);b[st]=1;
	for(int i=0;i<4;i++)cin>>goal.a[0][i];
	for(int i=3;i>=0;i--)cin>>goal.a[1][i];
	g=turn(goal);
	if(g==st)
	{
		cout<<0;return 0;
	}
	bfs();
	return 0;
}
bfs的优化方法还有如双端队列,双向bfs等,由于博主太弱,所以这里不多说。

bfs习题

  1. UVA1714 Keyboarding

  2. P1225 黑白棋游戏

posted @ 2020-11-06 10:31  洛桃  阅读(163)  评论(0编辑  收藏  举报