Top K 题目小结

博客介绍了在不同情况下求数组或链表第K小或第K大元素的方法。对于无序数组,可用最大堆或最小堆,也可用quick select算法;有序数组可直接取值;对于有序二维数组,用堆求解;行列有序的二维数组,用堆或线性查找。还提及求前K个最频繁元素可用Unordered_map搭配堆。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 不管是一维数组还是二维或多维数组求第K小或第K大元素,如果数组是无序的,那么

求第K小的元素就用最大堆,

求第K大的元素就用最小堆。

如果是一维数组,时间复杂度是O(NlogK)。如果是二维数组,时间复杂度是O(MNlogK)。

LintCode 461: Kth Smallest Numbers in Unsorted Array_纸上得来终觉浅 绝知此事要躬行的博客-优快云博客
注意,这里也可以是一个链表,或者M个链表,做法一样,时间复杂度也一样。

2. 不管是一维数组还是二维或多维数组求第K小或第K大元素,也可以用quick select算法。
数组不需要有序。
如果是一维数组,时间复杂度是O(N)。如果是二维数组,时间复杂度是O(MN)。

一维数组的quick select 见Lintcode 80 Median
LintCode 80: Median (QuickSelect经典题)_纸上得来终觉浅 绝知此事要躬行的博客-优快云博客

注意,quick select不能用链表。

3. 如果一维数组是有序的,直接取第k个或第n-k个元素即可。
如果二维数组每个数组都是有序的,那么
求第K小的元素就用最小堆,
求第K大的元素就用最大堆。

如果是二维矩阵,时间复杂度是O(M+KlogM)。这里M是建堆复杂度。

二维数组的例子见Lintcode 1874 Kth Smallest Element in a Specific Array
LintCode 1874: Kth Smallest Element in a Specific Array_纸上得来终觉浅 绝知此事要躬行的博客-优快云博客类似的是Merge K Sorted List 的例子
LintCode 104: Merge K Sorted Lists (经典题!)_纸上得来终觉浅 绝知此事要躬行的博客-优快云博客


4. 如果二维数组行列都是有序的,那么
求第K小的元素用最小堆
求第K大的元素用最大堆
以求第K小的元素用最小堆为例,思路是先放matrix[0][0],即最小的那个元素,然后将其pop(),再把它的正下方和右边的那个元素放进堆,
因为这两个肯定是仅比top大的那两个最小的元素。如此反复,一共pop掉k-1次,并且每次pop掉top后都把它正下方和右方的元素加进来。
时间复杂度O(klogk)。因为循环k遍,每次往里放最多2个数,所以heap最大size是2k,放了最多2k次,pop了k此,时间复杂度O(klogk)。

例子见 LintCode 401. Kth Smallest Number in Sorted Matrix 
LintCode 401. Kth Smallest Number in Sorted Matrix (堆经典好题!!!)_纸上得来终觉浅 绝知此事要躬行的博客-优快云博客

注意: 如果行列都是有序,找某个数可以用O(m+n)的线性方法解决,即把开始的点放在右上或左下,然后线性查找。

注意:这种行列皆有序的矩阵,如果是找某个数的话,从右上或左下的顶点开始找就可以,线性查找,时间复杂度O(n)。
LeetCode - The World's Leading Online Programming Learning Platform

5. 如果是求前K个most frequent的元素呢?光MaxHeap或MinHeap还不行,因为我们要记录每个元素的frequency。
一个方法是Unordered_map + MaxHeap
另一个方法是Unordered_map + MinHeap

LintCode 1281: Top K Frequent Elements (堆经典题)_纸上得来终觉浅 绝知此事要躬行的博客-优快云博客

6-3 最短路径(弗洛伊德算法) 分数 10 作者 王东 单位 贵州师范学院 试实现弗洛伊德最短路径算法。 函数接口定义: void ShortestPath_Floyed(AMGraph G); 其中 G 是基于邻接矩阵存储表示的有向图。 裁判测试程序样例: #include <iostream> using namespace std; #define MaxInt 32767 #define MVNum 100 typedef char VerTexType; typedef int ArcType; int Path[MVNum][MVNum]; int D[MVNum][MVNum]; typedef struct{ VerTexType vexs[MVNum]; ArcType arcs[MVNum][MVNum]; int vexnum,arcnum; }AMGraph; void CreateUDN(AMGraph &G);//实现细节隐藏 void ShortestPath_Floyed(AMGraph G); void DisplayPath(AMGraph G , int begin ,int temp ){ if(Path[begin][temp] != -1){ DisplayPath(G , begin ,Path[begin][temp]); cout << G.vexs[Path[begin][temp]] << "->"; } } int main(){ AMGraph G; char start , destination; int num_start , num_destination; CreateUDN(G); ShortestPath_Floyed(G); cin >> start >> destination; num_start = LocateVex(G , start); num_destination = LocateVex(G , destination); DisplayPath(G , num_start , num_destination); cout << G.vexs[num_destination]<<endl; cout << D[num_start][num_destination]; return 0; } /* 请在这里填写答案 */ 输入样例: 第1行输入结点数vexnum和边数arcnum。第2行输入vexnum个字符表示结点的值,接下来依次输入arcnum行,每行输入3个值,前两个字符表示结点,后一个数表示两个结点之间边的权值。最后一行输入源点及终点。 6 8 012345 0 5 100 0 2 10 0 4 30 1 2 5 2 3 50 3 5 10 4 3 20 4 5 60 0 5 输出样例: 第一行输出源点到终点的最短路径,第二行输出源点到终点的最短路径距离。 0->4->3->5 60 hh.png 代码长度限制 16 KB 时间限制 400 ms 内存限制 64 MB
最新发布
06-24
<think>我们被要求使用弗洛伊德算法(Floyd)实现最短路径计算,基于邻接矩阵存储的有向图。函数接口定义需要完成。弗洛伊德算法用于求解任意两点之间的最短路径。其基本思想是动态规划,通过逐步考虑所有顶点作为中间点,更新两点之间的最短路径。算法步骤:1.初始化距离矩阵D和路径矩阵P。-D[i][j]:顶点i到顶点j的最短路径长度。初始时为邻接矩阵的值(即边的权值,若没有直接边则设为无穷大,自己到自己是0)。-P[i][j]:记录从顶点i到顶点j的最短路径上,顶点j的前驱顶点(或者记录中间点)。初始时,如果i和j之间有边,则P[i][j]=i,否则为-1(或一个特殊值)。2.对于每一个顶点k(作为中间点),遍历所有顶点对(i,j):如果D[i][j]>D[i][k]+D[k][j],则更新D[i][j]=D[i][k]+D[k][j],并更新P[i][j]=P[k][j](或者记录中间点k,具体看如何设计路径记录)。注意:在更新路径矩阵P时,常见的做法是记录路径上j的前一个顶点,但也可以记录中间点。这里我们按照引用[4]中的描述,使用前驱矩阵,即P[i][j]表示从i到j的最短路径上j的前驱顶点。然而,引用[4]提到:将P初始化为一个矩阵,其中P[i][j]初始化为i(如果i和j有直接边)或者某个初始值(比如-1表示没有直接路径)。在更新时,当发现经过k使得路径更短,则更新P[i][j]为P[k][j](即从k到j的路径上j的前驱顶点)?实际上,另一种常见做法是更新为P[i][j]=P[k][j],但这样会丢失中间路径。更常见的做法是更新为k,或者更新为P[i][k]?我们需要明确。实际上,在Floyd算法中,路径矩阵P的更新方式有多种。这里我们采用:P[i][j]表示从i到j的最短路径上,j的前一个顶点。那么初始时,如果i和j有直接边,则P[i][j]=i;否则,可以设为-1。在更新时,如果经过k使得路径更短,则我们将P[i][j]更新为P[k][j](因为从k到j的路径上,j的前驱顶点就是P[k][j])。但这样会使得我们无法直接得到整个路径,因为这样更新后,路径实际上是i->...->k->...->j,所以j的前驱应该是从k到j的路径上j的前驱,也就是P[k][j]的值。另一种做法是记录中间点,但题目要求可能不同。根据引用[3]和[4],我们采用前驱矩阵的方式,即P[i][j]表示从i到j的最短路径上j的前驱顶点。具体步骤:初始化:D=邻接矩阵GP:一个二维数组,大小与G相同。对于每个i,j,若G[i][j]不是无穷大(即存在边),则P[i][j]=i;否则,P[i][j]=-1(表示没有路径)。然后,对于k从0到n-1(n为顶点数):对于i从0到n-1:对于j从0到n-1:if(D[i][k]+D[k][j]<D[i][j]){D[i][j]=D[i][k]+D[k][j];P[i][j]=P[k][j];//注意:这里更新为从k到j的路径上j的前驱顶点}但是,注意:这样更新P[i][j]为P[k][j]后,我们实际上记录了从k到j的路径中j的前驱,而整个路径是i->...->k->...->j。那么如何得到整个路径?我们可以通过递归或迭代的方式,从P[i][j]开始回溯,直到i。然而,这种更新方式有一个问题:当我们更新P[i][j]时,P[k][j]可能已经被更新过,即它可能不是k到j的直接前驱,而是更早的前驱。这符合要求,因为我们需要的是整个路径。但是,还有一种常见的写法是:P[i][j]=k,即记录中间点k。然后通过递归的方式重建路径。两种方法都可以,这里我们按照引用[4]的做法:使用前驱矩阵,且更新为P[k][j]。引用[4]原文:“在未分析任何顶点之前,我们将D命名为$D^{-1}$,其实它就是初始的图的邻接矩阵。将P命名为$P^{-1}$,初始化为图中所示的矩阵(即$P[i][j]=j$)。”但注意,他们的初始化是P[i][j]=j?这和我们想的不一样。再看:“接下来,其实也就是在D0的基础上进行变换,得到D1、D2、D3。当加入第一个中间顶点,即k=0时,我们就让所有的路径都经过这个顶点,如果经过这个顶点后路径变短,则修改D矩阵的值为更小的那个,同时修改P矩阵的值。修改P矩阵的值具体为:如果经过顶点k后,路径变短,则把$P[i][j]$的值修改为$P[k][j]$的值。”所以他们的做法是:P[i][j]=P[k][j]当更新时。因此,我们按照引用[4]的做法:初始化P矩阵为P[i][j]=i(如果i到j有直接路径)?但引用[4]的初始化是P[i][j]=j?这可能是笔误。实际上,在引用[4]的图中,初始化P矩阵是一个主对角线为0,其他位置为列号?比如P[i][j]=j?这样初始化的含义是:从i到j的路径上,j的前驱是j自己?这显然不对。我们再看其他引用。引用[3]中:“Path【】【】二维数组用于存储两顶点最短路径需要经过的上一个顶点下标”。所以初始化时,如果i到j有直接边,那么Path[i][j]=i;如果没有,则设为-1。因此,我们采用:初始化:D[i][j]=G[i][j];//邻接矩阵P[i][j]=(G[i][j]<INF&&i!=j)?i:-1;//注意:当i=j时,我们通常不需要前驱,但算法中自己到自己的距离为0,路径不需要,所以设为-1。或者对于i=j,我们也不设置前驱,设为-1。然后三重循环更新。注意:在循环中,如果D[i][k]和D[k][j]都是无穷大,那么就不更新。另外,我们通常用一个大数(如0x3f3f3f3f)表示无穷大。函数接口定义:我们需要实现ShortestPath_Floyed函数,参数是邻接矩阵表示的图(包括顶点数,邻接矩阵,以及两个用于返回的矩阵D和P)。函数计算每对顶点之间的最短路径,并填充D和P。根据题目要求,我们假设图的顶点数为n,邻接矩阵为G(n×n的二维数组),函数需要填充两个输出矩阵D和P(都是n×n的二维数组)。具体代码实现步骤:1.初始化:将G复制到D,并初始化P矩阵。2.三重循环:k从0到n-1,i从0到n-1,j从0到n-1。3.对于每个k,更新D[i][j]和P[i][j]。注意:需要防止溢出,所以判断D[i][k]和D[k][j]是否为无穷大,如果是则跳过。代码框架:voidShortestPath_Floyed(intn,int**G,int**D,int**P){//初始化D和Pfor(inti=0;i<n;i++){for(intj=0;j<n;j++){D[i][j]=G[i][j];//如果i到j有边(且权值不是无穷大),则P[i][j]初始化为i;否则为-1if(G[i][j]<INF&&i!=j){P[i][j]=i;}else{P[i][j]=-1;//没有直接路径,或者i=j}}}//三重循环for(intk=0;k<n;k++){for(inti=0;i<n;i++){for(intj=0;j<n;j++){//跳过中间点k不可达的情况if(D[i][k]!=INF&&D[k][j]!=INF&&D[i][k]+D[k][j]<D[i][j]){D[i][j]=D[i][k]+D[k][j];P[i][j]=P[k][j];//更新为从k到j的路径上j的前驱}}}}}但是,注意:这种更新P的方式(P[i][j]=P[k][j])在k==j的时候,P[k][j]是什么?初始时,当k=j时,P[k][j]即P[j][j]为-1(因为自己到自己的前驱我们设为-1),那么更新后P[i][j]就变成-1,这会导致路径回溯时无法得到完整路径?所以我们需要考虑。实际上,当k=j时,D[i][j]=D[i][j]+D[j][j]=D[i][j]+0,所以不会更新。因此不会出现这种情况。另一种常见做法:在更新P[i][j]时,我们记录的是中间点k,而不是前驱。这样我们可以通过递归来重建路径。但是题目要求没有明确,根据引用[4]我们使用前驱矩阵。然而,引用[4]中更新P[i][j]为P[k][j]后,当我们想要获取从i到j的路径时,可以通过P[i][j]得到j的前驱顶点,然后不断回溯直到i。例如,假设P[i][j]=x,那么j的前驱是x;然后看P[i][x]得到x的前驱,直到回溯到i。但是,这样回溯得到的路径是逆序的:j,x,...,i。所以需要反转。注意:在初始化时,如果i到j有直接边,那么P[i][j]=i,那么回溯:j的前驱是i,然后P[i][i]是-1(因为自己到自己的前驱不存在),所以停止。路径为i->j。当经过更新后,比如经过k,那么P[i][j]=P[k][j]。假设P[k][j]的值是m,那么j的前驱是m,然后我们再找P[k][m]?不对,这里注意,P[i][j]现在记录的是从k到j的路径上j的前驱,所以整个路径应该是:从i到k(这部分路径需要由P[i][k]来记录)和从k到j(由P[k][j]记录)?但是我们的P[i][j]只记录了j的前驱m,那么如何得到i到k的路径?实际上,在回溯路径时,我们并不需要知道k,而是直接通过P[i][j]得到j的前驱m,然后继续通过P[i][m]得到m的前驱?注意,这里我们使用的是同一个前驱矩阵P,但P[i][m]记录的是从i到m的最短路径上m的前驱,这个值可能已经更新过了。所以,当我们回溯路径时,对于任意点j,我们通过P[i][j]得到它的前驱,然后一直回溯到i。这样得到的是从i到j的路径(逆序)。这个路径是完整的。因此,上述更新P[i][j]为P[k][j]的做法是正确的。但是注意,在初始化时,我们设置P[i][j]为i(如果有直接边),所以回溯时,j的前驱是i,然后P[i][i]是-1,停止。路径就是i->j。在更新后,比如我们通过k,那么P[i][j]被更新为P[k][j]。假设P[k][j]的值为x,那么j的前驱是x。然后我们需要知道从i到x的路径,所以继续查P[i][x](注意不是P[k][x])。因为P[i][x]记录了从i到x的最短路径上x的前驱。所以这样回溯得到的路径就是i->...->x->j。因此,回溯时,我们只需要使用P[i]这一行,即从i出发到各个点的前驱顶点。所以,代码实现如上。但是,注意:在更新时,如果D[i][k]+D[k][j]等于D[i][j](即相等)时,我们不需要更新,因为已经存在一条同样短的路径。但如果我们希望记录一条路径,也可以更新(但通常不需要,因为最短路径可能有多条,我们记录任意一条即可)。所以这里我们只在更短的时候更新。另外,注意图的顶点编号从0到n-1。下面我们写出完整的函数代码(使用C语言)。假设邻接矩阵G中,INF表示无穷大(比如一个很大的数,例如0x3f3f3f3f)。注意:在初始化P矩阵时,对于i=j(对角线)的情况,我们设为-1,因为自己到自己没有路径(或者路径长度为0,不需要前驱)。代码实现:voidShortestPath_Floyed(intn,intG[][n],intD[][n],intP[][n]){//初始化D和Pfor(inti=0;i<n;i++){for(intj=0;j<n;j++){D[i][j]=G[i][j];if(i!=j&&G[i][j]<INF){P[i][j]=i;//从i到j有直接边,j的前驱是i}else{P[i][j]=-1;//没有直接边,或者i=j}}}for(intk=0;k<n;k++){for(inti=0;i<n;i++){for(intj=0;j<n;j++){//注意:避免溢出,判断INFif(D[i][k]<INF&&D[k][j]<INF&&D[i][k]+D[k][j]<D[i][j]){D[i][j]=D[i][k]+D[k][j];P[i][j]=P[k][j];//更新j的前驱为从k到j的路径上j的前驱}}}}}注意:这个函数假设D和P已经分配好空间(二维数组,大小为n×n)。调用者需要预先分配。但是,题目没有给出具体的函数接口形式,我们按照常见的写法。如果题目要求函数返回void,并且通过参数返回D和P,那么这样写就可以。另外,注意:在更新P[i][j]时,我们使用P[k][j]。这里k和j都是有效的顶点下标。测试:我们可以用一个小图测试。例如,有3个顶点,邻接矩阵:026INF03INFINF0初始化D为这个矩阵。初始化P:对于(0,0):-1;(0,1):0;(0,2):0?但是0->2有直接边,所以P[0][2]=0。对于(1,0):-1(不可达);(1,1):-1;(1,2):1。对于(2,0):-1;(2,1):-1;(2,2):-1。然后,k=0:以0为中间点。更新所有点对。比如i=1,j=0:不可更新(因为D[1][0]=INF)。i=0,j=0:跳过。等等。然后k=1:以1为中间点。考虑i=0,j=2:D[0][1]=2,D[1][2]=3,所以2+3=5<6,所以更新D[0][2]=5,同时P[0][2]=P[1][2]=1(因为P[1][2]=1,表示2的前驱是1)。然后k=2:没有更新。所以最终D[0][2]=5。回溯路径:从0到2:P[0][2]=1,所以2的前驱是1;然后P[0][1]=0,所以1的前驱是0;然后P[0][0]=-1,停止。所以路径是0->1->2。这符合预期。因此,代码实现正确。注意:如果存在负权边,Floyd算法也可以处理,但不能有负权回路(因为负权回路会导致最短路径不存在,因为可以无限绕圈)。在代码中,如果出现负权回路,则D[i][i]会变为负数(自己到自己的距离为负)。所以可以在算法结束后检查对角线,如果有负数则说明存在负权回路。但是题目要求的是有向图,且没有提到负权,所以暂时不考虑。下面我们写出完整的代码(包括注释)。</think>以下是基于邻接矩阵存储的有向图,使用弗洛伊德算法(Floyd)实现最短路径计算的完整代码实现。该算法计算图中所有顶点对之间的最短路径,并记录路径信息。 ### 弗洛伊德算法核心思想 1. **动态规划**:通过逐步添加中间顶点,更新顶点间的最短路径 2. **矩阵迭代**:维护两个矩阵: - `D[][]`:存储顶点间的最短路径长度 - `P[][]`:存储路径中顶点的前驱信息 3. **时间复杂度**:$O(n^3)$,适合中等规模图 ### 代码实现(C语言) ```c #include <stdio.h> #include <limits.h> #define INF INT_MAX // 定义无穷大 #define MAX_VERTEX 100 // 最大顶点数 void ShortestPath_Floyed(int n, int G[][MAX_VERTEX], int D[][MAX_VERTEX], int P[][MAX_VERTEX]) { // 初始化距离矩阵D和前驱矩阵P for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { D[i][j] = G[i][j]; // 初始化为邻接矩阵 if (G[i][j] != INF && i != j) { P[i][j] = i; // 存在直接路径:j的前驱是i } else { P[i][j] = -1; // 无直接路径或自身 } } } // 三重循环更新最短路径 for (int k = 0; k < n; k++) { // 中间点k for (int i = 0; i < n; i++) { // 起点i for (int j = 0; j < n; j++) { // 终点j // 跳过无穷大避免溢出 if (D[i][k] != INF && D[k][j] != INF && D[i][k] + D[k][j] < D[i][j]) { D[i][j] = D[i][k] + D[k][j]; // 更新最短距离 P[i][j] = P[k][j]; // 更新前驱节点 } } } } } // 辅助函数:打印路径 void PrintPath(int i, int j, int P[][MAX_VERTEX]) { if (P[i][j] == -1) { printf("无路径"); return; } int stack[MAX_VERTEX], top = -1; int cur = j; // 反向追踪路径并压栈 while (cur != i) { stack[++top] = cur; cur = P[i][cur]; } stack[++top] = i; // 加入起点 // 出栈打印正向路径 while (top >= 0) { printf("%d", stack[top--]); if (top >= 0) printf(" → "); } } ``` ### 算法说明 1. **初始化阶段**: - `D`矩阵初始化为邻接矩阵$G$ - `P`矩阵记录直接路径的前驱顶点($P[i][j] = i$) 2. **迭代更新阶段**: - 对每个中间顶点$k$,检查路径$i \rightarrow k \rightarrow j$ - 若满足$D[i][k] + D[k][j] < D[i][j]$则更新: $$D[i][j] \leftarrow D[i][k] + D[k][j]$$ $$P[i][j] \leftarrow P[k][j]$$ 3. **路径重建**: - 通过`P`矩阵回溯路径(见`PrintPath`函数) ### 使用示例 ```c int main() { // 示例图(4个顶点) int n = 4; int G[MAX_VERTEX][MAX_VERTEX] = { {0, 2, 6, 4}, {INF, 0, 3, INF}, {7, INF, 0, 1}, {5, INF, 12, 0} }; int D[MAX_VERTEX][MAX_VERTEX], P[MAX_VERTEX][MAX_VERTEX]; ShortestPath_Floyed(n, G, D, P); // 打印顶点0到3的最短路径 printf("最短路径长度: %d\n", D[0][3]); printf("路径: "); PrintPath(0, 3, P); return 0; } ``` ### 关键点说明 1. **负权边处理**: - 算法支持负权边 - 但图中**不能有负权环**(会导致无限循环) 2. **路径回溯**: - `P[i][j]`存储$j$在$i→j$路径上的前驱顶点 - 通过递归/栈实现路径重建 3. **空间复杂度**:$O(n^2)$(存储两个$n \times n$矩阵) ### 应用场景 1. 交通网络最短路径规划 2. 网络路由优化 3. 社交网络关系分析 4. 游戏地图寻路算法 > 该实现严格遵循弗洛伊德算法的经典设计[^4],通过动态规划逐步优化路径,最终得到全局最优解。算法中距离矩阵$D$和前驱矩阵$P$的协同更新是核心操作[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值