目录
今天也是为了cc,努力奋斗的一天ヾ(≧▽≦*)o
貌似保研面试的时候会问一些这样的问题。。。那我就整理一下吧。反正刷《算法笔记》的时候看见时间复杂度就记到这里呗。
时间复杂度概述
时间复杂度就是 问题规模
n
n
n 的函数,
T
(
n
)
=
O
(
f
(
n
)
)
T(n) = O(f(n))
T(n)=O(f(n)),常用的时间复杂度比较为:
O
(
1
)
<
O
(
l
o
g
n
)
<
O
(
n
)
<
O
(
n
l
o
g
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) <O(2^n) < O(n!) < O(n^n)
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
记忆方法:常对幂指阶。
图
1. Dijkstra
时间复杂度
没有堆优化的为 O ( V 2 + E ) O(V^2+E) O(V2+E),堆优化过后的为 O ( V l o g V + E ) O(VlogV+E) O(VlogV+E)
关键代码
void Dijkstra(int s){ //s为起点
fill(d,d+MAXV,INF); //fill函数将整个d数组赋值为INF(慎用memset)
d[s] = 0; //起点s到达自身的距离为0
for(int i=0;i<n;i++){
int u=-1,MIN=INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j=0;j<n;j++){ //找到未访问的顶点中d[]最小的
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
if(u == -1) return;//这个细节需要注意~~~
vis[u] = true; //标记u为已访问
//只有下面这个for与邻接矩阵的写法不同
for(int j=0;j<Adj[u].size();j++){
int v = Adj[u][j].v; //通过邻接表直接获得u能够到达的顶点v
if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
//如果v未访问 && 以u为中介点可以使d[v]更优
d[v] = d[u] + Adj[u][j].dis; //优化d[v]
}
}
}
}
堆优化版本
while(!q.empty()){
//每次找最小的
int u = q.top().v;
int cost = q.top().cost;
q.pop();
//如果已经被访问过了,这里需要注意!
if(vis[u] == true){
continue;
}
vis[u] = true;
//接下来进行更新
for(int j=0;j<adj[u].size();j++){
int v = adj[u][j].v;
int cost = adj[u][j].cost;
if(vis[v] == false && d[u]+cost < d[v]){
q.push(node(v,d[u]+cost));
d[v] = d[u]+cost;
}
}
}
2. Bellman-Ford
时间复杂度
O(VE)
,其中V是顶点个数,E是边数。
关键代码
for(i = 0;i < n - 1;i++){ //执行n-1轮操作,其中n为顶点数
for(each edge u->v){ //每轮操作都遍历所有边
if(d[u] + length[u -> v] < d[v]){ //以u为中介点可以使d[v]更小
d[v] = d[u] + length[u -> v]; //松弛操作
}
}
}
3. SPFA
时间复杂度
O(kE)
,其中E是图的边数,k是一个常数,在很多情况下k不超过2,可见这个算法在大部分数据时异常高效,并且经常性地优于堆优化的Dijkstra算法。但是如果图中有从源点可达的负环时,传统SPFA的时间复杂度会退化成O(VE)
。
关键代码
//主体部分
while(!Q.empty()){
int u = Q.front(); //队首顶点编号为u
Q.pop(); //出队
inq[u] = false; //设置u为不在队列中
//遍历u的所有邻接边v
for(int j=0;j<Adj[u].size();j++){
int v = Adj[u][j].v;
int dis = Adj[u][j].dis;
//松弛操作
if(d[u] + dis < d[v]){
d[v] = d[u] + dis;
if(!inq[v]){ //如果v不在队列中
Q.push(v); //v入队
inq[v] = true; //设置v为在队列中
num[v++]; //v的入队次数加一
if(num[v] >= n){
return false; //有可达负环
}
}
}
}
}
4. Floyd
时间复杂度
O ( V 3 ) O(V^3) O(V3),其中 V V V是顶点的个数。
关键代码
void Floyd(){
for(int k=0;k<n;k++){
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(dis[i][k] != INF && dis[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j]){
dis[i][j] = dis[i][k] + dis[k][j]; //找到更短的路径
}
}
}
}
}
5. prim
时间复杂度
O(V*V)
,其中邻接表实现prim算法可以通过堆优化使时间复杂度降为
O
(
V
l
o
g
V
+
E
)
O(VlogV+E)
O(VlogV+E)。
此外,
O
(
V
2
)
O(V^2)
O(V2)的时间复杂度也说明,尽量在图的顶点数目较少而边数较多的情况下(即稠密图)上使用prim算法。至于为什么prim算法得到的生成树一定是最小生成树,可以参考《算法导论》的相关证明。
关键代码
int prim(){ //默认0号为初始点,函数返回最小生成树的边权之和
fill(d,d+MAXV,INF); //fill函数将整个d数组赋值为INF(慎用memset)
d[0] = 0; //只有0号顶点到集合S的距离为0,其余为INF
int ans = 0; //存放最小生成树的边权之和
for(int i=0;i<n;i++){ //循环n次
int u = -1,MIN = INF; //u使d[u]最小,MIN存放该最小的d[u]
for(int j=0;j<n;j++){ //找到未访问的顶点中d[]最小的
if(vis[j] == false && d[j] < MIN){
u = j;
MIN = d[j];
}
}
//找不到小于INF的d[u],则剩下的顶点和集合S不连通
if(u == -1){
return -1;
}
vis[u] = true; //标记u为已访问
ans += d[u]; //将与集合S距离最小的边加入到最小生成树
for(int v=0;v<n;v++){
//v未访问 && u能到达v && 以u为中介点可以使v离集合S更加近
if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]){
d[v] = G[u][v]; //将G[u][v]赋值给d[v]
}
}
}
}
6. kruskal
时间复杂度
O
(
E
l
o
g
E
)
O(ElogE)
O(ElogE),
E
E
E为图的边数。
显然kruskal适合顶点数较多、边数较少的情况,这和prim算法恰恰相反。于是可以根据题目所给的数据范围来选择合适的算法,即如果是稠密图(边多),则用prim算法;如果是稀疏图(边少),则用kruskal算法。
关键代码
int father[N]; //并查集数组
int findFather(int x){ //并查集查询函数
...
}
//kruskal函数返回最小生成树的边权之和,参数n为顶点个数,m为图的边数
int kruskal(int n,int m){
//ans为所求边权之和,Num_Edge为当前生成树的边数
int ans = 0;
int Num_Edge = 0;
for(int i=1;i <= n;i++){ //假设题目中顶点范围是[1,n]
father[i] = i; //并查集初始化
}
sort(E,E+m,cmp); //所有边按边权从小到大排序
for(int i=0;i<m;i++){ //枚举所有边
int faU = findFather(E[i].u); //查询测试边两个端点所在集合的根结点
int faV = findFather(E[i].v);
if(faU != faV){ //如果不在一个集合中
father[faU] = faV; //合并集合(即将测试边加入到最小生成树中)
ans += E[i].cost; //边权之和增加测试边的边权
Num_Edge++; //当前生成树的边数加1
if(Num_Edge == n - 1){
break; //边数等于顶点数减1时结束算法
}
}
}
if(Num_Edge != n-1){
return -1; //无法连通时返回-1
}else{
return ans; //返回最小生成树的边权之和
}
}
拓扑排序
时间复杂度
O
(
V
+
E
)
O(V+E)
O(V+E)
如果DAG有
V
V
V个顶点,
E
E
E条边,在拓扑排序的过程中,搜索入度为零的顶点所需的时间是
O
(
V
)
O(V)
O(V),建立入度数组为
E
E
E。在正常情况下,每个顶点进一次队列,出一次队列,所需时间
O
(
V
)
O(V)
O(V)。每个顶点入度减1的运算共执行了
E
E
E次。所以总的时间复杂为
O
(
V
+
E
)
O(V+E)
O(V+E)。
关键代码
bool topologicalSort(){
//初始化
int num = 0; //记录加入拓扑序列的顶点数
queue<int> q;
for(int i=0;i<n;i++){
if(inDegree[i] == 0){
q.push(i); //将所有入度为0的顶点入队
}
}
//循环体
while(!q.empty()){
int u = q.front(); //取队首顶点u
//printf("%d",u); //此处可输出顶点u,作为拓扑序列中的顶点
q.pop();
for(int i=0;i<G[u].size();i++){
int v = G[u][i]; //u的后继结点v
inDegree[v]--; //顶点v的入度减1
if(inDegree[v] == 0){ //顶点v的入度减为0则入队
q.push(v);
}
}
G[u].clear(); //清空顶点u的所有出边(如无必要可不写)
num++; //加入拓扑序列的顶点数加1
}
if(num == n) return true; //加入拓扑序列的顶点数为n,说明拓扑排序成功
else return false; //加入拓扑排序的顶点数小于n,说明拓扑排序失败(即存在环)
}
动态规划
最大连续子序列和
时间复杂度
O
(
n
)
O(n)
O(n)
不使用DP的话,使用暴力是
O
(
n
3
)
O(n^3)
O(n3)或者
O
(
n
2
)
O(n^2)
O(n2)
关键代码
//边界
dp[0] = A[0];
for(int i=1;i<n;++i){
//状态转移矩阵
dp[i] = max(A[i],A[i]+dp[i-1]);
}
最长不下降子序列(LIS)
时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
不使用DP的话,使用暴力是
O
(
2
n
)
O(2^n)
O(2n)
关键代码
int ans = -1; //记录最大的dp[i]
for(int i=1;i <= n;++i){
dp[i] = 1; //边界初始条件(即先假设每个元素自成一个子序列)
for(int j=1;j<i;++j){
if(A[i] >= A[j] && (dp[j]+1>dp[i])){
dp[i] = dp[j] + 1; //状态转移方程,用以更新dp[i]
}
}
ans = max(ans,dp[i]);
}
最长公共子序列
时间复杂度
O
(
n
m
)
O(nm)
O(nm)
不使用DP的话,使用暴力是
O
(
2
n
+
m
⋅
m
a
x
(
n
,
m
)
)
O(2^{n+m}·max(n,m))
O(2n+m⋅max(n,m))
关键代码
//边界
for(int i=0;i<=lenA;i++){
dp[i][0] = 0;
}
for(int j=0;j<=lenB;j++){
dp[0][j] = 0;
}
//状态转移方程
for(int i=1;i<=lenA;i++){
for(int j=1;j<=lenB;j++){
if(A[i] == B[j]){
dp[i][j] = dp[i-1][j-1] + 1;
}else{
dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
}
}
}
//dp[lenA][lenB]是答案
printf("%d\n",dp[lenA][lenB]);
01背包
时间复杂度
O
(
n
V
)
O(nV)
O(nV),
V
V
V表示背包自身的容量。
不使用DP的话,使用暴力是
O
(
2
n
)
O(2^n)
O(2n)
空间复杂度
O ( V ) O(V) O(V)
关键代码
for(int i=1;i<=n;i++){
for(v = V; v >= w[i] ;v--){ //逆序枚举v
dp[v] = max(dp[v],dp[v-w[i]]+c[i]);
}
}