今天开始不定期写一写我对各种基本数据结构与算法的详解。
当初一无所知的我也是看着各位网上CSDN的博主写的博客慢慢的了解各种算法。
从最初的dijkstra算法,到后来的tarjan,从最初的set,map容器,到现在手写splay,线段树。
几乎99%的知识都来源于查看别人的CSDN博客学习。
所以,现在虽然不会经常做算法题和搞OI了,但是还是想把自己对各路算法的心得,回馈给大家。
希望能够给想要学习数据结构或是参加OI的同学一点启发~
那么第一次,就讲一讲最基础的图论,也是最广为流传的图论算法:dijkstra算法(以下简称dj算法)
dj算法解决的是单源最短路问题,也就是说给予起点st,求st到图上任意一点的距离。
在实现dj算法的过程中,我们需要有存图结构(邻接表与邻接矩阵),储存每个点到起点的距离(最开始初始化成无限大),visit数组(是否曾被选择过过作为基点)。
过程是这样的:每次选择离起点最近的点作为基点,以此来更新它的邻点。更新完之后,再选择离起点最近的点作为基点,再更新这个点的领点,如此往复……
现在想想,其实dj算法就是BFS+贪心,相信等你也写过100次以上的dj算法后,你也会与我有一样的感受。它每次选一个点,然后扩散(bfs)到它的邻点之后,再从所有点中,选出离起点最近的点,继续扩散出去。这样总共n-1次之后,图上所有点离起点的距离必然是最小的。(不证明了)。之所以是n-1次,是因为最后一个点没必要被选作基点了,选了它也不可能更新其他点的距离,因为,一个点一旦被选为基点,那么它到起点的最短路径的长度,我们就已经求出来了,不能再通过别的路径,来更新其中一个基点,因为dj算法是贪心算法,所以先求出来并且已经作为基点的点,它的dis值,一定是最小的。
可能叙述的不是很好,那么一边贴代码一边讲吧!
/* O(n^2)暴力dj算法,pre数组存具体路径 */
int g[N][N]; //邻接矩阵
bool visit[N];//是否已经被选作为基点
int dis[N], pre[N];//分别存储每个点到起点的距离,pre就是最短路径上,这个点的前一个点
void dijkstra(int start)
{
memset(dis, 0x3f, sizeof(visit));//初始化每个点的大小为一个大数
visit[start] = true; dis[start] = 0; pre[start] = start;
//第一次直接把起点标记为基点 起点到起点的距离为0
//起点的前一个点设置为本身(或者你设置为-1啥的都行)
int i, j;
int tempv = start;//tempv来存当前这轮迭代的基点
for (i = 1; i < n; i++)//n-1次循环
{
for(j = 1; j <= n; j++)//暴力搜索图中所有的点
{
if (!visit[j] && dis[tempv]+g[tempv][j] < dis[j])
//如果j没被选为基点过 并且 基点到起点的距离加基点到j点边的距离
//小于j点目前到起点的距离,那么就更新
{
dis[j] = dis[tempv]+g[tempv][j];
pre[j] = tempv;
//更新dis数组和前驱数组
}
}
int temp = 0x7fffffff;//找下一次迭代的新基点,就是选没当过基点,并且距离
//起点距离最小的点
for (i = 1; i <= n; i++)//暴力搜索每一个点
{
if (!visit[i])//如果没当过基点
{
if (dis[i] < temp)//不断找距离起点最小点
{
temp = dis[j];
tempv = i;//更新temp值并记录这个点的下标
}
}
}
visit[tempv] = true;//OK这个tempv点是目前离起点最近的并且没当过基点的点
//标记成true
}
}
总结起来就是:每次选一个点,更新邻点,循环n-1次就可以了,还是相当好记。
第一次直接选你要去作为起点的点。
时间复杂度显而易见,两个for循环嵌套,所以是n^2。
dj算法的缺点在于,它不能处理有负权值的边的图,比如某条边的长度为-1。
为什么呢,因为dj算法是贪心BFS,而BFS有一个特点,就是短视!
它只能看到与自己相邻的点的情况,但是对于远方,它就一脸蒙蔽了。
如果有一条边是负的,他不知道多走这一条边是能减小dis的,所以
dj算法不能处理带负权边的图。
要注意的是,邻接矩阵要初试化成一个大数,表示这两个点之间没边。
讲完了基础的邻接矩阵版dj算法,那么我们讲讲OI里常用的堆优化dj算法。
在竞赛里,你不堆优化,妥妥地TLE(time limit exceed)
因为竞赛里,你的点数(n),边数(m)基本都是1e5数量级的。
话不多说,直接上代码。
/* O(eloge)堆优化dj算法,在n的数量级>=1e5时必须采用这种堆优化+邻接表方式 */
struct node{
int p, w;
node(int a, int b):p(a), w(b){}
bool operator< (const node& b) const
{
return w > b.w;
}
};
vector<node> g[N];
priority_queue<node> sup;
void dijkstra(int start)
{
memset(dis, 0x3f, sizeof(dis));
dis[start] = 0; pre[start] = start;
sup.push(node(start, 0));
while (!sup.empty())
{
node front = sup.top();
sup.pop(); int tempv = front.p;
if (visit[tempv]) continue;
visit[tempv] = true;
for (int i = 0; i < g[tempv].size(); i++)
{
int p = g[tempv][i].p;
if (!visit[p] && dis[tempv]+g[tempv][i].w < dis[p])
{
dis[p] = dis[tempv]+g[tempv][i].w;
pre[p] = tempv;
sup.push(node(p, dis[p]));
}
}
}
}
细节我就不解释了,直接讲思路。
还是每次找基点,更新邻点。
为什么这种就可以是eloge,而上面那种是n^2呢。
原因在于下面这种用堆来优化了每次找离起点最近的点的时间复杂度。
并且用邻接表优化了对于每一个基点,更新它的所有邻边的时间复杂度(上面那个就是1扫到n,很花时间)
使用stl自带的堆priority_queue,大大降低了编程复杂度。
不过如果是手写二叉堆的话,那么时间复杂度会好一个常数,是elogn。
原因在于stl的堆不能更改堆中的节点,而二叉堆可以直接改堆中的节点,即松弛操作。
不过编程复杂度太高了,也不会有人考场真的手写二叉堆hhhh
总结一下呢就是,dj算法是一个优秀的最短路算法,在堆优化的情况下可以做到eloge。
如果题目中不是单源的最短路,那么可以每个点都作为起点跑一下dj算法,那么时间复杂就是
n^3或是neloge。
dj算法有很多变式,比如求最短路径的条数啊,加条件什么的啊。
那么你多做一点dj算法的题目,相信就能对之更加得心应手。