集训前一周(乌鸦坐飞机)

以下内容来自:老刘上课开的飞机、课本

感谢:刘神、岳神、吾大海亮


为准备即将到来的激动人心的集训,刘神强行给我们拉进度条,半天上(据说)人家一个月课程,这飞机坐的我(贼爽)

虽然老师一再强调不用担心,以后这些会再过一次,不过每天照样云里雾里、慌的一匹……

既然说是拉条,所以知识也没有讲的很细(我也没有理解很透),这里就稍微列一下知识点:

搜索

搜索是啥子玩意儿?有这么一句话:搜索的本质就是逐步试探,在试探过程中找到问题的解。(终于学到传说中的暴力出奇迹了)

多重循环、递归什么的说来就来

于是……

枚举法

当我们事先知道结果或者约束条件的范围的时候,如果范围不是很大,我们就用枚举法。

枚举方法:直接,二分。优化方法:减范围,降维度。

如果循环层数不固定,那就甩给递归。

floodfill以及迷宫类问题

floodfill种子染色法表示一种填充的过程,就像一滴墨水滴在了纸上,开始只是一个小点,然后慢慢扩散,最后填满整个区域。迷宫类问题一般都在地图上找最短路或方法数,一般解法就是dfs、bfs遍历。

注意:边界、怎么走(方向数组)

但如果要求所有可能到达的情况,这种“一条道走到黑”的做法显然不行,所以就讲到了回溯。

回溯

当所有情况都搜索过且都无法到达目标的时候,退回到上一个出发点,不断回头寻找目标。是一个十分优秀的,有条不紊的搜索问题答案的方法。

(就是容易爆炸)

经典例题:八皇后。dfs+回溯,若此路不通,就回到上一个路口。

动态规划

(1)数据储存(2)搜索(3)记忆化搜索(4)递推

基础

(1)dp三要素:阶段,状态,状态转移。

(2)最优化原理和最优子结构:一个最优化策略的子策略总是最优的。dp就是由局部最优解推出最终最优解的过程。

(3)无后效性:考虑问题的过程中,如果告诉了你当前状态的最优值,我们可以无视当前状态是怎么来的,都不会影响到之后的答案。

(4)子问题的重叠性:dp解决了搜索中重复的子问题,是搜索的优化。

(5)dp问题的一般解题步骤

    1、判断问题是否具有最优子结构性质,若不具备则不能用动态规划。

    2、把问题分成若干个子问题(分阶段)。

    3、建立状态转移方程(递推公式)。

    4、找出边界条件。

    5、将已知边界值代入方程。

    6、递推求解。

dp方程在时间上不用担心,但是空间上就有点尴尬了。如果发现dp需要空间太大,那么你就可以滚了……

滚动数组:有些问题在状态转移的时候,第i阶段的最优值一般只与i-1阶段有关系,所以只需要两个数组来记录当前阶段和上一个阶段的状态,滚起来……

0/1背包问题

这里请允许我介绍一位大佬:通往新世界的大门

(1)二维数组表示

    1、定义状态:f[i][c]表示前i件物品恰放入一个容量为c的背包可以获得的最大价值。

    2、状态转移方程:f[i][c]=max(f[i][c],f[i-1][c])……(不选这件物品)

                                            max(f[i][c],f[i-1][c-w[i]]+v[i])……(选择这件物品)

核心代码:

int c[],w[];//c[i]表示i占空间,w[i]表示i价值
int f[][];//f[i][v]表示“将前i件物品放入容量为v的背包中”这个子问题
for(int i=1;i<=n;i++)
	for(int v=1;v<=V;v++)
	{
		f[i][v]=f[i-1][v];
		if(v-c[i]>=0)
			f[i][v]=max(f[i][v],f[i-1][v-c[i]]+w[i])
	}
f[n][v]

(2)一维数组表示

发现f[i][c]只与f[i-1]这一层有关系,所以,就可以把i这维数组优化掉。第i层的f[c]只与第i-1层的f[c]和f[c-w[i]]有关,所以在求f[c]的时候,要保证f[c-w[i]]还是第i-1阶段的最优值,所以c要从C开始倒着推,保证f[c]左边的状态没有被第i个物品更新。

核心代码:

for(int i=1;i<=n;i++)
	for(int v=V;v>=1;v--)//倒着来保证前面的不被更新
	{
		if(v-c[i]>=0)
			f[v]=max(f[v],f[v-c[i]]+w[i])
	}
f[n][v]

(3)一维之下的一个常数优化

没必要让循环下限为0.

int bound,sumw=0;
for(int i=1;i<=n;i++)
{
	sumw+=w[i];
	bound=max(C-sumw,w[i]);
	for(int c=C;c>=bound;c--)
		if(c>=w[i])f[c]=max(f[c],f[c-w[i]]+v[i]);
}

(4)初始化的细节

如果要求“恰好装满”,那么初始化时应该让f[0]=0,其他的f[i]=-INF。这样就可以知道是否有解了。

如果不用恰好,那么应该让f的所有元素都置0。

完全背包问题

最优秀的算法:

//内外for可互换 
for(int i=1;i<=n;i++)
	for(int c=0;c<=C;c++)
		if(c>=w[i])f[c]=max(f[c].f[c-w[i]]+v[i]);

凭什么把0/1背包的for循环倒过来就行了?因为0/1倒着推是保证物品只用一次,正着推,f[c-w[i]]的值已经被第i个物品更新过了,所以第i个物品可以随便用。

二维费用的背包问题

(多开一维数组不就好了)

多重背包问题
混合背包问题
分组的背包问题
有依赖的背包问题
特殊要求
背包问题的搜索解法

(不存在的……就讲了一天时间……)

双进程类

就是有两个同时进行的决策,两个决策不是相互独立,而是相互影响的。

区间动态规划

前面的状态设置一般是:前i个的什么什么最优值,而大区间最优值是由小区间的最优值推导出来的,那么f[i][j]就表示从第i个元素到第j个元素这个区间的最优值是多少。

图论

二话不说,来到最后一个大槛——图论。

图的存储

有邻接矩阵、邻接表、边表三种

矩阵写着简单,但是点一多就容易炸……无向图中a[i][j]=1表示有边,=0表示无边;有向图中a[i][j]表示边ij的权值(无边即无限大)

链表直接上模板:

//声明 
struct edge
{
	int y,v,next;//y表示这条边的终点编号,v是权值 
				 //next表示同起点下条边的编号是多少 
}e[maxn+100];//边表 
int lin[maxn+100];//起点表lin[i]表示由i出去的第一条边的下标是多少 
int len=0;//len表示有len条边
//读入
void insert(int xx,int yy,int vv)//xx为起点,yy为终点,vv为权值 
{
	e[++len].next=lin[xx];lin[xx]=len;
	e[len].y=yy;e[len].v=vv;
}
void init()
{
	scanf("%d%d",&n,&m);
	memset(e,0,sizeof(e));
	memset(lin,0,sizeof(lin));
	for(int i=1;i<=n;i++)
	{
		int xx,yy,vv;
		scanf("%d%d%d",&xx,&yy,&vv);
		insert(xx,yy,vv);
		insert(yy,xx,vv);//这里插入的是无向图,所以两条边都要插入 
	}
}

边表就是只记录所有边的信息:

//声明
struct edge
{
	int x,y; //起点和终点 
	int v;	 //权值 
} e[maxn+100]; 
图的遍历

直接上模板(真心懒):

//邻接矩阵的dfs遍历
void dfs(int k)
{
	vis[k]=1;
	for(int i=1;i<=n;i++)
		if(a[k][i]&&(!vis[i]))
			dfs(i);
}//O(n^2)

//邻接表的dfs遍历
void bfs(int k)
{
	vis[k]=1;
	for(int i=lin[k];i;i=e[i].next)
	{
		if(!vis[e[i].y])
			dfs(e[i].y);
	}
} 

//主程序
int main()
{
	memset(vis,0,sizeof(vis));
	for(int i=1;i<=n;i++)
		if(!vis[i])dfs(i);
} 

//求无向的连通分量
sumn=0;
for(int i=1;i<=n;i++)
	if(!vis[i])
	{
		sumn++;
		dfs(i);
	} 
printf("%d\n",sumn);

//邻接矩阵的bfs遍历
void bfs(int i)
{
	memset(q,0,sizeof(q));
	int head=0,tail=1;
	q[1]=i;vis[i]=true;
	while(head<tail)
	{
		k=q[++head];cout<<k;
		for(int j=1;j<=n;j++)
			if(a[k][j]&&!vis[j])
			{
				q[++tail]=j;
				vis[j]=true;
			}
	}
} 

//邻接链表的bfs遍历
int q[maxn+200];
int head=0,tail=1;
void bfs(int k)
{
	q[tail]=k;
	vis[k]=1;
	for(head<=tail)
	{
		int tn=q[++head];
		for(int i=lin[tn];i;i=e[i].next)
		{
			if(!vis[e[i].y])
			{
				vis[e[i].y]=1;
				q[++tail]e[i].y;
			}
		}
	}
}

图的传递闭包即是判断无向图的连通性:(i到j,i到k到j)

for(int i=1;i<=n;i++)can[i][i]=1;
for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			can[i][j]=can[i][j]||(can[i][k]&&can[k][j]);
图的最短路径

三角形性质:dis[x]+len[x][y]>=dis[y];

松弛:若在处理过程中,有两点x、y出现不符合“三角形定理”,则可以改进一下——松弛:if(dis[x]+len[x][y]<dis[y])dis[y]=dis[x]+len[x][y]。

四大最短路(还是spfa最好用)

(1)每对顶点(任意两点)之间的最短路径——floyed(弗洛伊德算法)//任意两点,无负权回路

void init()
{
    cin>>n;
	cin>>st>>en;
    memset(dis,10,sizeof(dis));
    for(int i=1;i<=n;i++)
		dis[i][i]=0;
    while(cin>>x>>y>>z)
		dis[x][y]=dis[y][x]=z;   
}
void floyed()
{
    for(int k=1;k<=n;k++)
    	for(int i=1;i<=n;i++)
        	for(int j=1;j<=n;j++)
            	if(d[i][k]+d[k][j]<d[i][j])
                	d[i][j]=d[i][k]+d[k][j]
}
int main()
{
    init();
	floyed();
	cout<<dis[st][en];
    retrun 0;
}

(2)一个顶点到其他顶点的最短路径(单源最短路径)——dijkstra(迪杰斯特卡算法)//单源,非负

void dijkstra(int st);
{
    for(int i=1;i<=n;i++)
		dis[i]=a[st,i];
    memset(f,false,sizeof(f));
    f[st]=1;
	dis[st]=0;
    for(int i=1;i<n;i++)
    {
        int min=1000000000,k=0;
        for(int j=1;j<=n;j++)
            if(!f[j]&&(dis[j]<min))
				min=dis[j],k=j;
        if(k==0)return;//已经找不到了
        f[k]=1;//把k加入集合
        for(int j=1;j<=n;j++)//三角形迭代,更新最短距离
            if(!f[j]&&(dis[k]+a[k][j]<dis[j]))
				dis[j]=dis[k]+a[k][j];
    }
}
cout<<dis[en];
(3)最短路径(求单源点到其他点的最短距离,判断是否有负环)——Bellman-ford算法//用边表
bool Bellmanford(int st)
{
	memset(dis,10,sizeof(dis));
	dis[st]=0;
	bool rel=0;
	for(int i=1;i<=n;i++)  //最多迭代n 
	{
		rel=0;  //是否有松弛标志 
		for(int j=1;j<=len;j++)
			if(dis[e[j].next]+e[j].v<dis[e[j].y])
				dis[e[j].y]=e[j].v+dis[e[j].next],rel=1;
		if(!rel)return 0;  //没有一次松弛,结束 
	}
	return 1;  //迭代了n次,有负圈 
}
(4)最短路径(对Bellman-ford算法迭代的改进)——SPFA算法//从上次刚被松弛过的点x,来看看x能不能松弛其他点,用队列
void SPFA(int s)
{
	memset(dis,0x7f,sizeof(dis));
	memset(vis,0,sizeof(vis));
	dis[s]=0;vis[s]=1;q[1]=s;
	head=0,tail=1;
	while(head<tail)
	{
		int tn=q[++head];vis[tn]=0;
		int te=lin[tn];
		for(int i=te;i;i=e[i].next)
		{
			int tmp=e[i].y;
			if(dis[tn]+e[i].v<dis[tmp])
			{
				dis[tmp]=dis[tn]+e[i].v;
				if(!vis[tmp])
				{
					q[++tail]=tmp;
					vis[tmp]=1;
				}
			}
		}
	}
}
最小生成树

Prim(普里姆)算法

将G剪切成两个集合A、B,A中只有一个点r;取最小权的交叉边(x,y),x属于B,y属于B;将y加入A;如果已经加了n-1条边,结束,否则,转到第三步。

void Prim()
{
	memset(dis,10,sizeof(dis));
	for(int i=1;i<=n;i++)dis[i]=a[s][i];//s随意 
	memset(vis,0,sizeof(vis));
	vis[s]=1;sumn=0;
	for(int i=2;i<=n;i++)
	{
		int minn=a[0][0],c=0;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&dis[j]<minn)
			{
				minn=dis[j];
				c=j;
			}
		vis[c]=1;
		sumn+=minn;
		for(int j=1;j<=n;j++)
			if(a[c][j]<dis[j]&&!vis[j])
				dis[j]=a[c][j];
	}
}

(2)Kruskal(克鲁斯卡尔)算法

判断边(x,y)的两个顶点x,y在图mst中是否已经连通。如果已经连通,加入边将形成环;否则,不形成环。//用到并查集

int grtfather(){}
void merge(){}
bool judge(){}
void Kruskal()
{
	for(int i=1;i<=n;i++)f[i]=i;
	sort(e+1,e+m+1,mycmp);
	cal=0;
	for(int i=1;i<=len;i++)
	{
		int v=getfather(e[i].x);
		int u=getfather(e[i].y);
		if(v!=u)
		{
			merge(v,n);
			if(++cal==n-1)
			{
				printf("%d\n",e[i].v);
				return;
			}
		}
	}
}
拓扑排序(topsort)

一般用队列来维护拓扑排序:

(1)初始化,先把所有入度为0的点进队;

(2)把和队头q[head]有边相邻的点的入度依次-1(去边),如果减过之后,该点的入度为0,那么此点也入队;

(3)head++,goto(2)。

void topsort()
{
	head=0;tail=0;
	for(int i=1;i<=n;i++)if(!id[i])q[++tail]=i;
	while(head<tail)
	{
		head++;
		for(int i=1;i<=n;i++)
			if(a[q[head]][i])
			{
				id[i]--;
				if(!id[i])q[++tail]=i;
			}
	}
}

没错,这些就是六天的课程(爽歪歪)

虽然刷题量严重跟不上进度(正常正常……)但是基本能够听懂,勉强可以敲个模板……

虽然累,但是毕竟这种坐飞机的机会不多……

第一次写这么长的博客,写的很水,自己都感觉好多地方没解释清楚(没办法——懒)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值