【蓝桥杯】49362.《视频相关度计算》

发布于:2024-12-23 ⋅ 阅读:(13) ⋅ 点赞:(0)

视频相关性计算

问题描述

小蓝作为异世界最大流媒体网站 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 部视频的相关性结果。

代码实现

Python 实现

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())

JAVA实现

import java.util.LinkedList;
import java.util.Scanner;

public class DinicAlgorithm {
    // 定义一个极大值,用于表示类似无穷大的概念,例如在网络流中表示流量无上限等情况
    static final int INF = Integer.MAX_VALUE;
    // 定义节点数量的上限值,这里是一个预估的较大值,方便处理不同规模的图数据
    static final int N = 105;
    // 定义边数量的上限值,同样是预估的较大值,便于应对不同输入情况
    static final int M = 1005;
    // head数组用于存储邻接表的表头指针,初始化为 -1,表示每个节点初始时没有出边
    static int[] head = new int[N];
    // d数组用于存储节点的距离标号,初始化为0,后续在广度优先搜索分层图构建中会更新
    static int[] d = new int[N];
    // 边的结构体,用于表示图中的边
    static class Edge {
        int u;  // 边的起点
        int v;  // 边的终点
        int flow;  // 边的流量(在这里可以类比视频相关性中的权重)
        int next;  // 指向下一条边的指针,用于构建邻接表结构

        public Edge(int u, int v, int flow, int next) {
            this.u = u;
            this.v = v;
            this.flow = flow;
            this.next = next;
        }
    }
    // 边的数组,用于存储图的边信息
    static Edge[] e = new Edge[M << 1];
    static int k = 0;
    // n, m, s, t分别用于存储输入的节点数、边数、起始节点、目标节点
    static int n, m, s, t;

    // 向图中添加边的函数
    static void add(int u, int v, int f) {
        e[k] = new Edge(u, v, f, head[u]);
        head[u] = k++;
        e[k] = new Edge(v, u, 0, head[v]);
        head[v] = k++;
    }

    // 广度优先搜索函数,用于构建分层图,找到从起始节点s到其他节点的最短距离(以边的数量衡量)
    static boolean bfs() {
        for (int i = 0; i < N; i++) {
            d[i] = 0;
        }
        LinkedList<Integer> que = new LinkedList<>();
        que.add(s);
        d[s] = 1;
        while (!que.isEmpty()) {
            int x = que.poll();
            for (int i = head[x]; i!= -1; i = e[i].next) {
                if (d[e[i].v] == 0 && e[i].flow > 0) {
                    que.add(e[i].v);
                    d[e[i].v] = d[x] + 1;
                }
            }
            if (d[t]!= 0) {
                return true;
            }
        }
        return false;
    }

    // 深度优先搜索函数,用于在分层图的基础上,沿着距离标号递增的路径寻找增广路,并更新流量
    static int dfs(int u, int flow) {
        if (u == t) {
            return flow;
        }
        int res = flow;
        for (int i = head[u]; i!= -1 && res > 0; i = e[i].next) {
            if (d[e[i].v] == d[u] + 1 && e[i].flow > 0) {
                int temp = dfs(e[i].v, Math.min(res, e[i].flow));
                if (temp == 0) {
                    d[e[i].v] = 0;
                }
                res -= temp;
                e[i].flow -= temp;
                e[i ^ 1].flow += temp;
            }
        }
        return flow - res;
    }

    // Dinic算法主函数,通过不断调用bfs构建分层图,然后调用dfs寻找增广路来计算最大流(在这里类比视频相关性总和)
    static int dinic() {
        int ans = 0;
        while (bfs()) {
            while (true) {
                int temp = dfs(s, INF);
                if (temp == 0) {
                    break;
                }
                ans += temp;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 读取输入的节点数、边数、起始节点、目标节点
        n = scanner.nextInt();
        m = scanner.nextInt();
        s = scanner.nextInt();
        t = scanner.nextInt();
        // 初始化head数组为 -1
        for (int i = 0; i < N; i++) {
            head[i] = -1;
        }
        // 循环读取每一条边的信息,并调用add函数将边添加到图中
        for (int i = 0; i < m; i++) {
            int u = scanner.nextInt();
            int v = scanner.nextInt();
            int f = scanner.nextInt();
            add(u, v, f);
        }

        // 调用dinic函数计算并输出第S部视频到第T部视频的相关性(通过网络流算法计算得到的类似最大流的值)
        System.out.println(dinic());

        scanner.close();
    }
}

C++实现

#include <iostream>
#include <queue>
#include <cstring>
using namespace std;

// 定义一个极大值,用于表示类似无穷大的概念,例如在网络流中表示流量无上限等情况
const int INF = 0x3f3f3f3f;  
// 定义节点数量的上限值,这里是一个预估的较大值,方便处理不同规模的图数据
const int N = 105;  
// 定义边数量的上限值,同样是预估的较大值,便于应对不同输入情况
const int M = 1005;  

// head数组用于存储邻接表的表头指针,初始化为 -1,表示每个节点初始时没有出边
int head[N];  
// d数组用于存储节点的距离标号,初始化为0,后续在广度优先搜索分层图构建中会更新
int d[N];  
// 边的结构体,用于表示图中的边
struct Edge {
    int u;  // 边的起点
    int v;  // 边的终点
    int flow;  // 边的流量(在这里可以类比视频相关性中的权重)
    int next;  // 指向下一条边的指针,用于构建邻接表结构
};
// 边的数组,用于存储图的边信息
Edge e[M << 1];  
int k = 0;
// n, m, s, t分别用于存储输入的节点数、边数、起始节点、目标节点
int n, m, s, t;  

// 向图中添加边的函数
void add(int u, int v, int f) {
    e[k].u = u;
    e[k].v = v;
    e[k].flow = f;
    e[k].next = head[u];
    head[u] = k++;

    e[k].u = v;
    e[k].v = u;
    e[k].flow = 0;
    e[k].next = head[v];
    head[v] = k++;
}

// 广度优先搜索函数,用于构建分层图,找到从起始节点s到其他节点的最短距离(以边的数量衡量)
bool bfs() {
    memset(d, 0, sizeof(d));
    queue<int> que;
    que.push(s);
    d[s] = 1;
    while (!que.empty()) {
        int x = que.front();
        que.pop();
        for (int i = head[x]; i!= -1; i = e[i].next) {
            if (!d[e[i].v] && e[i].flow > 0) {
                que.push(e[i].v);
                d[e[i].v] = d[x] + 1;
            }
        }
        if (d[t]) return true;
    }
    return false;
}

// 深度优先搜索函数,用于在分层图的基础上,沿着距离标号递增的路径寻找增广路,并更新流量
int dfs(int u, int flow) {
    if (u == t) return flow;
    int res = flow;
    for (int i = head[u]; i!= -1 && res > 0; i = e[i].next) {
        if (d[e[i].v] == d[u] + 1 && e[i].flow > 0) {
            int temp = dfs(e[i].v, min(res, e[i].flow));
            if (temp == 0) d[e[i].v] = 0;
            res -= temp;
            e[i].flow -= temp;
            e[i ^ 1].flow += temp;
        }
    }
    return flow - res;
}

// Dinic算法主函数,通过不断调用bfs构建分层图,然后调用dfs寻找增广路来计算最大流(在这里类比视频相关性总和)
int dinic() {
    int ans = 0;
    while (bfs()) {
        while (true) {
            int temp = dfs(s, INF);
            if (temp == 0) break;
            ans += temp;
        }
    }
    return ans;
}

int main() {
    // 读取输入的节点数、边数、起始节点、目标节点
    cin >> n >> m >> s >> t;
    // 初始化head数组为 -1
    memset(head, -1, sizeof(head));
    // 循环读取每一条边的信息,并调用add函数将边添加到图中
    for (int i = 0; i < m; i++) {
        int u, v, f;
        cin >> u >> v >> f;
        add(u, v, f);
    }

    // 调用dinic函数计算并输出第S部视频到第T部视频的相关性(通过网络流算法计算得到的类似最大流的值)
    cout << dinic() << endl;

    return 0;
}

C实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义一个极大值,用于表示类似无穷大的概念,例如在网络流中表示流量无上限等情况
#define INF 0x3f3f3f3f
// 定义节点数量的上限值,这里是一个预估的较大值,方便处理不同规模的图数据
#define N 105
// 定义边数量的上限值,同样是预估的较大值,便于应对不同输入情况
#define M 1005

// 边结构体,用于表示图中的边
typedef struct Edge {
    int u;  // 边的起点
    int v;  // 边的终点
    int flow;  // 边的流量(在这里可以类比视频相关性中的权重)
    int next;  // 指向下一条边的指针,用于构建邻接表结构
} Edge;

// 全局变量声明
Edge e[M << 1];  // 边数组,用于存储图的边信息
int head[N];  // head数组用于存储邻接表的表头指针,初始化为 -1,表示每个节点初始时没有出边
int d[N];  // d数组用于存储节点的距离标号,初始化为0,后续在广度优先搜索分层图构建中会更新
int k = 0;
int n, m, s, t;  // n, m, s, t分别用于存储输入的节点数、边数、起始节点、目标节点

// 向图中添加边的函数
void add(int u, int v, int f) {
    e[k].u = u;
    e[k].v = v;
    e[k].flow = f;
    e[k].next = head[u];
    head[u] = k++;

    e[k].u = v;
    e[k].v = u;
    e[k].flow = 0;
    e[k].next = head[v];
    head[v] = k++;
}

// 广度优先搜索函数,用于构建分层图,找到从起始节点s到其他节点的最短距离(以边的数量衡量)
int bfs() {
    memset(d, 0, sizeof(d));
    int que[N];
    int front = 0, rear = 0;
    que[rear++] = s;
    d[s] = 1;
    while (front < rear) {
        int x = que[front++];
        for (int i = head[x]; i!= -1; i = e[i].next) {
            if (!d[e[i].v] && e[i].flow > 0) {
                que[rear++] = e[i].v;
                d[e[i].v] = d[x] + 1;
            }
        }
        if (d[t]) return 1;
    }
    return 0;
}

// 深度优先搜索函数,用于在分层图的基础上,沿着距离标号递增的路径寻找增广路,并更新流量
int dfs(int u, int flow) {
    if (u == t) return flow;
    int res = flow;
    for (int i = head[u]; i!= -1 && res > 0; i = e[i].next) {
        if (d[e[i].v] == d[u] + 1 && e[i].flow > 0) {
            int temp = dfs(e[i].v, (res < e[i].flow? res : e[i].flow));
            if (temp == 0) d[e[i].v] = 0;
            res -= temp;
            e[i].flow -= temp;
            e[i ^ 1].flow += temp;
        }
    }
    return flow - res;
}

// Dinic算法主函数,通过不断调用bfs构建分层图,然后调用dfs寻找增广路来计算最大流(在这里类比视频相关性总和)
int dinic() {
    int ans = 0;
    while (bfs()) {
        while (1) {
            int temp = dfs(s, INF);
            if (temp == 0) break;
            ans += temp;
        }
    }
    return ans;
}

int main() {
    // 读取输入的节点数、边数、起始节点、目标节点
    scanf("%d %d %d %d", &n, &m, &s, &t);
    // 初始化head数组为 -1
    memset(head, -1, sizeof(head));
    // 循环读取每一条边的信息,并调用add函数将边添加到图中
    for (int i = 0; i < m; i++) {
        int u, v, f;
        scanf("%d %d %d", &u, &v, &f);
        add(u, v, f);
    }

    // 调用dinic函数计算并输出第S部视频到第T部视频的相关性(通过网络流算法计算得到的类似最大流的值)
    printf("%d\n", dinic());

    return 0;
}

代码运行结果

>>> 
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
>>> 

在这里插入图片描述