视频相关性计算
问题描述
小蓝作为异世界最大流媒体网站 LanTube 的高级算法工程师,他想要实现更加精准的视频推荐服务来满足用户的喜好。
其中,**“视频的相关性”**是一个重要指标,它代表了两个视频 A 到 B 的关联程度,记作 f(A, B)。
现在我们给出两个视频 相关性 的定义:若 A 视频,在通过一系列相关联视频构成的路径中,可以关联到 B 视频,则称此条路径为 相关路径,而相关性 f(A,B)等于每一条相关路径上最小相关性的总和。
例如:视频 A 与视频 B,C 相关,相关性f(A,B)= 10,f(A,C)= 4;视频 B 与视频D 相关,f(B,D)=5;视频 C 与视频 D 相关,f(C,D)=3。则在相关路径A→B→D 中,最小相关性为5,在相关路径A→C→D 中,最小相关性为 3,则 f(A,D)=5+3=8。
现在给出了 N 部视频,序号依次为1~ N。并已知 M 条关于第a部视频到第b部视频相关性f(a,b),其中a≠b。求第 S 部视频到第 T 部视频的相关性。
注明:原题说法有误,f(A,C)= 2,f(C,D)=3,则在相关路径A→C→D 中,最小相关性应该为 2,则 f(A,D)=5+2=7。所以7才是正确的最小相关性。此处为了不引起误解,将f(A,C)=2修改为f(A,C)=4。
输入格式
第一行包含四个正整数 N,M,S,T,代表有 N 个视频,有 M 对视频的相关性,求第 S 和第 T 部视频相关性。
接着下面 M 行,每行有三个整数 α,b, f(a,b),代表第 α 部到第 b部视频相关性为 f(a,b)。
输出格式
输出一个正整数,代表第 S 和第 T 部视频相关性。
输入样例
7 8 2 7
2 1 50
2 3 40
1 4 40
3 4 50
4 5 20
4 6 40
5 7 30
6 7 40
输出样例
60
说明
题目中存在两条相关路径
·2→1→4→5→7,该路径中最小相关性为 20。
·2→3→4→6→7,该路径中最小相关性为 40。
综上,第2个节点到第7个节点的相关性为 60。
数据范围
对于所有数据,保证 2<N<100,1 <M<1000,1≤S≠T≤N,1≤a,b<N,f(a,b)< 108
题目解析
这道题本质上是图的路径搜索问题。
假设有一个图,其中有N个节点,M条边,已知出发点S和结束点T,求经过S和T的路径有多少条,这些路径即为“相关路径”。
遍历这些相关路径,求得每条路径上最小权重值,该值即为该路径的最小相关性。
将每条路径的最小相关性累加。
如下图所示:
根据题目的要求,题目中存在两条相关路径
·2→1→4→5→7,该路径中最小的权重的边为4→5,权重为 20。
·2→3→4→6→7,该路径中最小的权重的边为2→3,权重为 40。
则2→7的相关性为两条路径的最小权重之和=20+40=60。
解题思路
整体思路概述
代码在整体上采用网络流算法中的 Dinic 算法来解决视频相关性计算的问题,本质上是将视频看作图中的节点,视频之间的相关性看作边的权重,通过寻找从起始视频节点(S)到目标视频节点(T)的所有相关路径,并计算每条路径上的最小相关性之和来得到最终结果。
数据结构与初始化部分
定义常量与全局变量:
定义了 INF 表示一个极大值(通过 sys.maxsize 获取),用于表示流量的无穷大概念(在网络流算法里常用)。
定义了 N 和 M 分别作为节点数量和边数量的上限值,初始化了 head(用于存储邻接表的表头指针)、d(用于存储节点的距离标号)、e(存储边的信息的列表)、k(边的索引计数)以及 n、m、s、t(分别对应输入的节点数、边数、起始节点、目标节点,后续会重新赋值)等全局变量。
定义节点类 Node:
用于表示图中的边,包含了起点 u、终点 v、边的流量(即为相关性权重)flow 以及指向下一条边的指针 next,通过这个类可以方便地构建图的邻接表结构。
初始化函数 add:
用于向图中添加边,每添加一条边,实际上会在邻接表中添加两条互为反向的边(一条正向表示从 u 到 v 的边,另一条反向边流量初始化为 0,这是网络流算法处理反向边的常见做法),同时更新 k(边的索引)以及对应的邻接表表头 head。
核心算法部分(基于 Dinic 算法)
广度优先搜索函数 bfs:
首先将所有节点的距离标号 d 初始化为 0,然后将起始节点 s 加入队列 que,并标记其距离标号 d[s] 为 1。
接着进入循环,不断从队列中取出节点 x,遍历其所有出边(通过邻接表结构,从 head[x] 开始),对于那些未被标记距离标号(d[e[i].v] == 0)且边的流量(权重)大于 0 的邻接节点 e[i].v,将其加入队列,并更新其距离标号为当前节点距离标号加 1(d[e[i].v] = d[x] + 1)。
只要在搜索过程中发现目标节点 t 的距离标号被更新了,就表示找到了从 s 到 t 的一条增广路径,返回 True;否则当队列为空还没找到时,返回 False。这个过程实际上是在构建分层图,为后续的深度优先搜索找增广路做准备。
深度优先搜索函数 dfs:
以当前节点 u 和可流经的流量 flow 作为参数,当 u 等于目标节点 t 时,说明找到了一条从 s 到 t 的完整路径,直接返回当前流量 flow。
否则,尝试沿着距离标号递增(d[e[i].v] == d[u] + 1)且边的流量大于 0 的边进行深度优先搜索,调用 dfs 函数递归地找下一层节点,每次传递的流量是当前剩余流量 res 和当前边的流量 e[i].flow 中的较小值(min(res, e[i].flow))。
如果递归返回的流量 temp 为 0,表示当前边无法继续拓展增广路了,将对应节点的距离标号置为 0(d[e[i].v] = 0);然后更新剩余流量 res,同时减少当前边正向边的流量、增加反向边的流量(这是网络流算法中反向边的作用体现,用于回退流量和寻找其他增广路)。
最后返回本次从 u 出发实际能推送的流量(flow - res)。
Dinic 算法主函数 dinic:
在一个循环中不断调用 bfs 函数构建分层图,如果存在增广路(bfs 返回 True),则进入内层循环不断调用 dfs 函数找增广路并更新最大流(这里的最大流值累加对应相关性的计算,每次找到一条增广路就相当于找到了一条相关路径,并把路径上能通过的流量累加起来,类比于相关路径的最小相关性累加),直到找不到增广路(dfs 返回 0)为止,最后返回累加得到的相关性总和(也就是最大流的值)。
主程序部分
输入处理:
通过 input().split() 获取输入的节点数 n、边数 m、起始节点 s 和目标节点 t,然后循环 m 次,每次读取一条边的起点 u、终点 v 和相关性权重 f,并调用 add 函数将边添加到图中。
结果输出:
调用 dinic 函数计算并直接输出第 S 部视频到第 T 部视频的相关性结果。
代码实现
import os
import sys
# 引入双端队列,用于后续广度优先搜索中存储节点
from collections import deque
# 定义一个极大值,用于表示类似无穷大的概念,例如在网络流中表示流量无上限等情况
INF = sys.maxsize
# 定义节点数量的上限值,这里是一个预估的较大值,方便处理不同规模的图数据
N = 105
# 定义边数量的上限值,同样是预估的较大值,便于应对不同输入情况
M = 1005
# head列表用于存储邻接表的表头指针,初始化为 -1,表示每个节点初始时没有出边
head = [-1] * N
# d列表用于存储节点的距离标号,初始化为0,后续在广度优先搜索分层图构建中会更新
d = [0] * N
# e列表用于存储图的边信息,每条边用一个Node类实例表示
e = []
# k用于记录边的索引,方便在邻接表中添加和查找边
k = 0
# n, m, s, t分别用于存储输入的节点数、边数、起始节点、目标节点,初始先赋值为0,后续会重新读取输入进行赋值
n, m, s, t = 0, 0, 0, 0
# 定义Node类,用于表示图中的边
class Node:
def __init__(self, u, v, flow, next):
# 边的起点
self.u = u
# 边的终点
self.v = v
# 边的流量(在这里可以类比视频相关性中的权重)
self.flow = flow
# 指向下一条边的指针,用于构建邻接表结构
self.next = next
# 向图中添加边的函数
def add(u, v, f):
global k
# 创建一条从u到v的边,将其添加到边列表e中,并更新对应的邻接表表头指针等信息
e.append(Node(u, v, f, head[u]))
head[u] = k
k += 1
# 同时添加反向边(在网络流算法中反向边用于回退流量等操作),流量初始化为0
e.append(Node(v, u, 0, head[v]))
head[v] = k
k += 1
# 广度优先搜索函数,用于构建分层图,找到从起始节点s到其他节点的最短距离(以边的数量衡量)
def bfs():
# 初始化所有节点的距离标号为0
for i in range(N):
d[i] = 0
# 创建队列,并将起始节点s加入队列
que = deque([s])
# 标记起始节点s的距离标号为1,表示距离起始点的距离为1(按边的数量算)
d[s] = 1
# 只要队列不为空,就进行循环,不断扩展已访问的节点范围
while que:
# 取出队列头部的节点
x = que.popleft()
# 获取当前节点x的邻接表表头指针
i = head[x]
# 遍历当前节点x的所有出边
while i!= -1:
# 如果邻接节点e[i].v还未被标记距离标号(即还未访问过)且当前边的流量大于0(表示可以通过这条边继续扩展路径)
if not d[e[i].v] and e[i].flow > 0:
# 将邻接节点加入队列
que.append(e[i].v)
# 更新邻接节点的距离标号,为当前节点距离标号加1
d[e[i].v] = d[x] + 1
# 获取下一条边的指针,继续遍历当前节点的其他出边
i = e[i].next
# 如果在搜索过程中,目标节点t的距离标号已经被更新了(说明找到了从s到t的一条路径),则返回True
if d[t]:
return True
# 如果队列为空还未找到到目标节点t的路径,则返回False
return False
# 深度优先搜索函数,用于在分层图的基础上,沿着距离标号递增的路径寻找增广路,并更新流量
def dfs(u, flow):
# 如果当前节点u就是目标节点t,说明找到了一条从起始节点到目标节点的完整路径,返回当前可流经的流量
if u == t:
return flow
# res用于记录当前从节点u出发还能分配的剩余流量,初始为传入的flow
res = flow
# 获取当前节点u的邻接表表头指针
i = head[u]
# 遍历当前节点u的所有出边,只要还有剩余流量可以分配且还有出边可遍历
while i!= -1 and res > 0:
# 如果邻接节点的距离标号等于当前节点距离标号加1(符合分层图中距离递增的要求,即沿着增广路的方向)且当前边的流量大于0(表示可以通过这条边推送流量)
if d[e[i].v] == d[u] + 1 and e[i].flow > 0:
# 递归调用dfs函数,尝试沿着这条边向邻接节点推送流量,推送的流量取剩余流量res和当前边流量e[i].flow中的较小值
temp = dfs(e[i].v, min(res, e[i].flow))
# 如果递归返回的流量为0,说明从邻接节点那边无法继续推送流量了,将邻接节点的距离标号置为0(相当于标记此路不通,后续搜索不再考虑)
if temp == 0:
d[e[i].v] = 0
# 更新剩余流量,减去已经推送出去的流量temp
res -= temp
# 减少当前边正向边的流量,表示已经推送了部分流量过去
e[i].flow -= temp
# 增加当前边反向边的流量,这是网络流算法中利用反向边回退流量、寻找其他增广路的关键操作
e[i ^ 1].flow += temp
# 获取下一条边的指针,继续遍历当前节点的其他出边
i = e[i].next
# 返回从当前节点u出发实际能推送出去的流量(即初始传入的流量减去剩余未推送出去的流量)
return flow - res
# Dinic算法主函数,通过不断调用bfs构建分层图,然后调用dfs寻找增广路来计算最大流(在这里类比视频相关性总和)
def dinic():
ans = 0
# 只要通过bfs能构建出分层图(即存在增广路),就进入循环
while bfs():
# 在每次构建好分层图后,不断调用dfs寻找增广路并更新最大流,直到找不到增广路(dfs返回0)为止
while True:
temp = dfs(s, INF)
if temp == 0:
break
ans += temp
return ans
if __name__ == "__main__":
# 读取输入的节点数、边数、起始节点、目标节点
n, m, s, t = map(int, input().split())
# 循环读取每一条边的信息,并调用add函数将边添加到图中
for _ in range(m):
u, v, f = map(int, input().split())
add(u, v, f)
# 调用dinic函数计算并输出第S部视频到第T部视频的相关性(通过网络流算法计算得到的类似最大流的值)
print(dinic())
代码运行结果
>>>
7 8 2 7
2 1 50
2 3 40
1 4 40
3 4 50
4 5 20
4 6 40
5 7 30
6 7 40
60
>>>