最小权顶点覆盖问题
最小权顶点覆盖问题
问题描述
给定一个赋权无向图 G=(V, E) ,每个顶点 v \in V 都有权值 w(v) 。如果 U \subseteq V ,且对任意 (u, v) \in E 有 u \in U 或 v \in U ,就称 U 为图 G 的一个顶点覆盖。 G 的最小权顶点覆盖是指 G 中所含顶点权之和最小的顶点覆盖。
算法设计
对于给定的无向图 G ,设计一个优先队列式分支限界法,计算 G 的最小权顶点覆盖。
数据输入
由文件 input.txt 给出输入数据。第 1 行有 2 个正整数 n 和 m ,表示给定的图 G 有 n 个顶点和 m 条边,顶点编号为 1, 2, ⋯, n 。第 2 行有 n 个正整数表示 n 个顶点的权。接下来的 m 行中,每行有 2 个正整数 u 和 v ,表示图 G 的一条边 (u, v) 。
结果输出
将计算的最小权顶点覆盖的顶点权之和以及最优解输出到文件 output.txt。文件的第 1 行是最小权顶点覆盖顶点权之和;第 2 行是最优解 x_i (1 \leq i \leq n) , x_i = 0 表示顶点 i 不在最小权顶点覆盖中, x_i = 1 表示顶点 i 在最小权顶点覆盖中。
示例
输入文件示例 (input.txt)
7
110011110010
16
24
25
36
45
46
67
输出文件示例 (output.txt)
13
1011001
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#define MAXN 100 // 最大顶点数
#define MAXE 1000 // 最大边数
#define INF INT_MAX // 无穷大
// 边结构体
typedef struct {
int u, v; // 边的两个端点
} Edge;
// 优先队列节点(搜索节点)
typedef struct {
int cost; // 已选顶点的权值和 g(n)
int h; // 启发式函数值 h(n)
int f; // 优先级 f(n) = cost + h
int selected[MAXN]; // 顶点选择状态 1-选 0-未选
int covered[MAXE]; // 边覆盖状态 1-覆盖 0-未覆盖
} Node;
// 全局变量
int n, m; // 顶点数 边数
int w[MAXN]; // 顶点权值
Edge edges[MAXE]; // 边集合
int best_cost; // 最优解的权值和
int best_selected[MAXN]; // 最优解的顶点选择状态
int node_count; // 生成节点计数(用于调试)
// 优先队列(最小堆)
Node heap[1000000];
int heap_size;
// 交换两个节点
void swap(Node *a, Node *b) {
Node tmp = *a;
*a = *b;
*b = tmp;
}
// 堆的上浮操作
void heapify_up(int idx) {
while (idx > 0) {
int parent = (idx - 1) / 2;
if (heap[parent].f <= heap[idx].f) break;
swap(&heap[parent], &heap[idx]);
idx = parent;
}
}
// 堆的下沉操作
void heapify_down(int idx) {
while (1) {
int left = 2 * idx + 1;
int right = 2 * idx + 2;
int smallest = idx;
if (left < heap_size && heap[left].f < heap[smallest].f)
smallest = left;
if (right < heap_size && heap[right].f < heap[smallest].f)
smallest = right;
if (smallest == idx) break;
swap(&heap[idx], &heap[smallest]);
idx = smallest;
}
}
// 节点入队
void enqueue(Node node) {
heap[heap_size++] = node;
heapify_up(heap_size - 1);
node_count++;
}
// 节点出队
Node dequeue() {
Node top = heap[0];
heap[0] = heap[--heap_size];
heapify_down(0);
return top;
}
// 计算启发式函数值 h(n)
int calc_heuristic(int covered[]) {
int h = 0;
for (int i = 0; i < m; i++) {
if (!covered[i]) {
// 未覆盖边取两端点权值较小值累加
int u = edges[i].u - 1;
int v = edges[i].v - 1;
h += (w[u] < w[v]) ? w[u] : w[v];
}
}
return h;
}
// 检查当前节点是否为完整顶点覆盖
int is_complete(int covered[]) {
for (int i = 0; i < m; i++) {
if (!covered[i]) return 0;
}
return 1;
}
// 分支限界法主搜索函数
void branch_and_bound() {
// 初始化最优解
best_cost = INF;
memset(best_selected, 0, sizeof(best_selected));
heap_size = 0;
node_count = 0;
// 根节点初始化
Node root;
root.cost = 0;
memset(root.selected, 0, sizeof(root.selected));
memset(root.covered, 0, sizeof(root.covered));
root.h = calc_heuristic(root.covered);
root.f = root.cost + root.h;
enqueue(root);
while (heap_size > 0) {
Node cur = dequeue();
// 剪枝:当前 f 值 >= 最优解,无需扩展
if (cur.f >= best_cost) continue;
// 检查是否为完整覆盖,更新最优解
if (is_complete(cur.covered)) {
if (cur.cost < best_cost) {
best_cost = cur.cost;
memcpy(best_selected, cur.selected, sizeof(cur.selected));
}
continue;
}
// 找到第一条未覆盖的边,作为分支依据
int e_idx = -1;
for (int i = 0; i < m; i++) {
if (!cur.covered[i]) {
e_idx = i;
break;
}
}
if (e_idx == -1) continue;
int u = edges[e_idx].u - 1;
int v = edges[e_idx].v - 1;
// 分支1:选择顶点 u
Node child1 = cur;
if (!child1.selected[u]) {
child1.selected[u] = 1;
child1.cost += w[u];
// 标记所有与 u 相关的边为已覆盖
for (int i = 0; i < m; i++) {
if (edges[i].u - 1 == u || edges[i].v - 1 == u) {
child1.covered[i] = 1;
}
}
}
child1.h = calc_heuristic(child1.covered);
child1.f = child1.cost + child1.h;
if (child1.f < best_cost) {
enqueue(child1);
}
// 分支2:选择顶点 v
Node child2 = cur;
if (!child2.selected[v]) {
child2.selected[v] = 1;
child2.cost += w[v];
// 标记所有与 v 相关的边为已覆盖
for (int i = 0; i < m; i++) {
if (edges[i].u - 1 == v || edges[i].v - 1 == v) {
child2.covered[i] = 1;
}
}
}
child2.h = calc_heuristic(child2.covered);
child2.f = child2.cost + child2.h;
if (child2.f < best_cost) {
enqueue(child2);
}
}
}
int main() {
// 读取输入
FILE *fin = fopen("input.txt", "r");
if (!fin) {
printf("无法打开输入文件 input.txt\n");
return 1;
}
fscanf(fin, "%d%d", &n, &m);
for (int i = 0; i < n; i++) {
fscanf(fin, "%d", &w[i]);
}
for (int i = 0; i < m; i++) {
fscanf(fin, "%d%d", &edges[i].u, &edges[i].v);
}
fclose(fin);
// 执行分支限界搜索
branch_and_bound();
// 输出结果
FILE *fout = fopen("output.txt", "w");
if (!fout) {
printf("无法打开输出文件 output.txt\n");
return 1;
}
fprintf(fout, "%d\n", best_cost);
for (int i = 0; i < n; i++) {
fprintf(fout, "%d ", best_selected[i]);
}
fclose(fout);
// 控制台输出信息
printf("搜索完成!\n");
printf("生成节点总数:%d\n", node_count);
printf("最小权顶点覆盖总和:%d\n", best_cost);
printf("最优解选择状态:");
for (int i = 0; i < n; i++) {
printf("%d ", best_selected[i]);
}
printf("\n");
return 0;
}
最小权支配集问题
问题定义
给定一个赋权无向图 G = (V, E) ,其中:
· V = {1, 2, . . . , n} 是顶点集合,包含 n 个顶点
· E 是边集合,包含 m 条边
· 每个顶点 v ∈ V 有一个非负权值 w(v) > 0
如果子集 D \subseteq V 满足:对于任意顶点 v \in V ,要么 v \in D ,要么存在一个顶点 u \in D 使得边 (u, v) \in E ,则称 D 为图 G 的一个支配集。
图 G 的最小权重支配集是指 G 中所有支配集中顶点权重之和最小的支配集。
算法设计
对于给定的无向图 G ,设计一个优先队列式分支限界法,计算 G 的最小权重支配集。
数据输入
输入数据由文件 input.txt 给出,格式如下:
n m
w(1) w(2) ... w(n)
u₁ v₁
u₂ v₂
...
uₘ vₘ
其中:
· 第1行包含2个正整数 n 和 m ,表示给定的图 G 有 n 个顶点和 m 条边
· 第2行包含 n 个正整数,表示 n 个顶点的权重
· 接下来的 m 行,每行包含2个正整数 u 和 v ,表示图 G 的一条边 (u, v)
结果输出
将计算的最小权重支配集的顶点权重之和以及最优解输出到文件 output.txt,格式如下:
最小权重支配集顶点权重之和
x₁ x₂ ... xₙ
其中:
· 第1行是最小权重支配集的顶点权重之和
· 第2行是最优解 x_i (1 \leq i \leq n) , x_i = 0 表示顶点 i 不在最小权重支配集中, x_i = 1 表示顶点 i 在最小权重支配集中
示例
输入文件示例 (input.txt)
7 9
3 5 2 4 6 1 3
1 2
1 3
2 4
2 5
3 6
3 7
4 5
5 6
6 7
图结构说明:
· 顶点数:7,边数:9
· 顶点权重:w(1)=3, w(2)=5, w(3)=2, w(4)=4, w(5)=6, w(6)=1, w(7)=3
· 边连接情况如上所示
输出文件示例 (output.txt)
8
0 1 1 0 0 0 0
解释:
· 最小权重支配集的权重之和为8
· 选择的顶点集合为 {2, 3},对应向量为 0 1 1 0 0 0 0
· 顶点2支配顶点1,2,4,5
· 顶点3支配顶点3,6,7
· 所有顶点都被支配(每个顶点要么在{2,3}中,要么与{2,3}中至少一个顶点相邻)
问题特性
- NP难问题:最小权重支配集问题是NP难的,即使所有权重都为1。
- 近似难度:对于一般图,不存在多项式时间的 (1-\varepsilon)\ln n 近似算法,除非 NP \subseteq DTIME(n^{O(\log \log n)}) 。
- 贪婪近似:存在 O(\log n) 近似算法,基于贪心选择单位成本覆盖最多未支配顶点的顶点。
与最小权顶点覆盖的区别
方面 最小权顶点覆盖 最小权重支配集
覆盖对象 边(每条边至少一个端点在集合中) 顶点(每个顶点要么在集合中,要么有邻居在集合中)
约束条件 对每条边(u,v):x_u + x_v ≥ 1 对每个顶点v:∑_{u∈N[v]} x_u ≥ 1,其中N[v]是v的闭邻域
近似比 2-近似 O(log n)-近似
二分图 多项式时间可解 NP难
应用领域
最小权重支配集问题在以下领域有重要应用:
- 无线传感器网络:选择最少数量的传感器(考虑部署成本)来监控所有区域
- 社交网络:选择最少的有影响力的人(考虑成本)来影响整个网络
- 设施选址:选择最少的设施点(考虑建设成本)服务所有客户
- 网络安全:选择最少的监控点(考虑部署成本)来监控整个网络
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#define MAXN 25 // 最大节点数,适配 n ≤ 20
#define MAXM 200 // 最大边数
#define INF INT_MAX // 无穷大,代表不可达状态
// 边结构体
typedef struct {
int u, v;
} Edge;
// 优先队列节点(搜索状态)
typedef struct {
int cost; // 已选节点的总成本 g(n)
int h; // 启发式下界 h(n)
int f; // 优先级 f(n) = cost + h
int selected[MAXN]; // 节点选择状态 1-部署 0-不部署
int covered[MAXN]; // 节点覆盖状态 1-覆盖 0-未覆盖
} Node;
// 全局变量
int n, m;
int w[MAXN]; // 节点部署成本
Edge edges[MAXM];
int adj[MAXN][MAXN]; // 邻接矩阵:adj[u][v]=1 表示 u 和 v 相邻
int best_cost; // 最优解总成本
int best_selected[MAXN]; // 最优解选择方案
int heap_size; // 优先队列大小
Node heap[1000000]; // 优先队列(最小堆)
// 交换两个节点
static void swap(Node *a, Node *b) {
Node tmp = *a;
*a = *b;
*b = tmp;
}
// 堆上浮操作,维护最小堆性质
static void heapify_up(int idx) {
while (idx > 0) {
int parent = (idx - 1) / 2;
if (heap[parent].f <= heap[idx].f) break;
swap(&heap[parent], &heap[idx]);
idx = parent;
}
}
// 堆下沉操作,维护最小堆性质
static void heapify_down(int idx) {
while (1) {
int left = 2 * idx + 1;
int right = 2 * idx + 2;
int smallest = idx;
if (left < heap_size && heap[left].f < heap[smallest].f)
smallest = left;
if (right < heap_size && heap[right].f < heap[smallest].f)
smallest = right;
if (smallest == idx) break;
swap(&heap[idx], &heap[smallest]);
idx = smallest;
}
}
// 节点入队
static void enqueue(Node node) {
heap[heap_size++] = node;
heapify_up(heap_size - 1);
}
// 节点出队(返回堆顶,即 f 值最小的节点)
static Node dequeue() {
Node top = heap[0];
heap[0] = heap[--heap_size];
heapify_down(0);
return top;
}
// 计算启发式下界 h(n):对未覆盖节点,取自身或相邻节点的最小成本累加
static int calc_heuristic(int covered[]) {
int h = 0;
for (int u = 1; u <= n; u++) {
if (covered[u]) continue;
// 找 u 自身和相邻节点的最小成本
int min_w = w[u];
for (int v = 1; v <= n; v++) {
if (adj[u][v] && w[v] < min_w) {
min_w = w[v];
}
}
h += min_w;
}
return h;
}
// 检查是否所有节点都被覆盖
static int is_all_covered(int covered[]) {
for (int u = 1; u <= n; u++) {
if (!covered[u]) return 0;
}
return 1;
}
// 部署节点 u 后,更新覆盖状态
static void update_cover(int covered[], int u, int val) {
covered[u] = val;
for (int v = 1; v <= n; v++) {
if (adj[u][v]) {
covered[v] = val;
}
}
}
// 分支限界法主搜索函数
static void branch_and_bound() {
// 初始化最优解
best_cost = INF;
memset(best_selected, 0, sizeof(best_selected));
heap_size = 0;
// 初始化根节点:无部署,无覆盖
Node root;
root.cost = 0;
memset(root.selected, 0, sizeof(root.selected));
memset(root.covered, 0, sizeof(root.covered));
root.h = calc_heuristic(root.covered);
root.f = root.cost + root.h;
enqueue(root);
while (heap_size > 0) {
Node cur = dequeue();
// 剪枝:当前估计总成本 ≥ 已知最优解,无需扩展
if (cur.f >= best_cost) continue;
// 找到完整覆盖方案,更新最优解
if (is_all_covered(cur.covered)) {
if (cur.cost < best_cost) {
best_cost = cur.cost;
memcpy(best_selected, cur.selected, sizeof(best_selected));
}
continue;
}
// 选择第一个未覆盖的节点作为分支点
int u = -1;
for (int i = 1; i <= n; i++) {
if (!cur.covered[i]) {
u = i;
break;
}
}
if (u == -1) continue;
// 分支1:在 u 节点部署监控
Node child1 = cur;
if (!child1.selected[u]) {
child1.selected[u] = 1;
child1.cost += w[u];
update_cover(child1.covered, u, 1);
}
child1.h = calc_heuristic(child1.covered);
child1.f = child1.cost + child1.h;
if (child1.f < best_cost) {
enqueue(child1);
}
// 分支2:不在 u 部署,选择 u 的一个相邻节点 v 部署
for (int v = 1; v <= n; v++) {
if (!adj[u][v] || child1.selected[v]) continue;
Node child2 = cur;
child2.selected[v] = 1;
child2.cost += w[v];
update_cover(child2.covered, v, 1);
child2.h = calc_heuristic(child2.covered);
child2.f = child2.cost + child2.h;
if (child2.f < best_cost) {
enqueue(child2);
}
}
}
}
// 读取输入
static void read_input() {
FILE *fin = fopen("input.txt", "r");
if (!fin) {
printf("Error: 无法打开 input.txt\n");
exit(1);
}
fscanf(fin, "%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
fscanf(fin, "%d", &w[i]);
}
// 初始化邻接矩阵
memset(adj, 0, sizeof(adj));
for (int i = 0; i < m; i++) {
int u, v;
fscanf(fin, "%d%d", &u, &v);
edges[i] = (Edge){u, v};
adj[u][v] = 1;
adj[v][u] = 1;
}
fclose(fin);
}
// 写入输出
static void write_output() {
FILE *fout = fopen("output.txt", "w");
if (!fout) {
printf("Error: 无法打开 output.txt\n");
exit(1);
}
if (best_cost == INF) {
fprintf(fout, "-1\n");
fprintf(fout, "无解\n");
} else {
fprintf(fout, "%d\n", best_cost);
for (int i = 1; i <= n; i++) {
fprintf(fout, "%d ", best_selected[i]);
}
fprintf(fout, "\n");
}
fclose(fout);
}
int main() {
read_input();
branch_and_bound();
write_output();
// 控制台打印结果
printf("最小部署成本: %d\n", best_cost);
printf("部署方案: ");
for (int i = 1; i <= n; i++) {
printf("%d ", best_selected[i]);
}
printf("\n");
return 0;
}