马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
关押罪犯(贪心+二分答案+染色法判定二分图/扩展域并查集)
标题形貌
S 城现有两座牢狱,一共关押着 N 名罪犯,编号分别为 1∼N。他们之间的关系天然也极反面谐。许多罪犯之间以致积怨已久,如果客观条件具备则随时大概发作辩说。我们用“怨气值”(一个正整数值)来表现某两名罪犯之间的愤恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为 c 的罪犯被关押在同一牢狱,他们俩之间会发生摩擦,并造成影响力为 c 的辩说变乱。
每年年末,警员局会将本年内牢狱中的全部辩说变乱按影响力从大到小排成一个列表,然后上报到 S 城 Z 市长那里。公务繁忙的 Z 市长只会去看列表中的第一个变乱的影响力,如果影响很坏,他就会思量撤换警员局长。
在具体观察了 N 名罪犯间的抵牾关系后,警员局长以为压力巨大。他预备将罪犯们在两座牢狱内重新分配,以求产生的辩说变乱影响力都较小,从而保住本身的乌纱帽。假设只要处于同一牢狱内的某两个罪犯间有愤恨,那么他们肯定会在每年的某个时间发生摩擦。
那么,应怎样分配罪犯,才华使 Z 市长看到的谁人辩说变乱的影响力最小?这个最小值是多少?
输入格式
每行中两个数之间用一个空格隔开。第一活动两个正整数 N,M,分别表现罪犯的数目以及存在愤恨的罪犯对数。接下来的 M 行每活动三个正整数 aj,bj,cj,表现 aj 号和 bj 号罪犯之间存在愤恨,其怨气值为 cj。数据包管 1<aj<bj≤N,0<cj≤109,且每对罪犯组合只出现一次。
输特殊式
共一行,为 Z 市长看到的谁人辩说变乱的影响力。如果本年内牢狱中未发生任何辩说变乱,请输出 0。
分析:
第一种方法:
利用并查集:
变量寄义先容
N: 界说了节点数的上限,这里设置为 20010,表现最多有 20010 个节点。
p[N]: 并查集的父数组,用于纪录每个节点的父节点,实现路径压缩优化。
e 布局体:
u, v: 边的两个顶点。
w: 边的权值。
重载 < 运算符,使得布局体按权值 w 从大到小排序。
a[N*5]: 存储全部边的数组,最多可容纳 5*N 条边,满足标题中大概的边数范围。
enemy[N]: 纪录每个节点的一个敌对节点。比方,enemy = v 表现 u 和 v 是仇人。
read() 函数:快速读取整数,制止 cin 的痴钝输入。
find(int x): 并查集的查找函数,返回节点 x 的根节点,并举行路径压缩。
merge(int x, int y): 归并节点 x 和 y 所在的聚集。
n, m: 输入的节点数和边数。
贪心计谋与排序
将边按权值从大到小排序,优先处置处罚权值大的边。如许,当第一次出现辩说(两个仇人在同一聚集)时,当前边的权值即为答案,包管了答案的最小性。
并查集与仇人关系处置处罚
处置处罚边 (u, v) 时,若 u 已有仇人 enemy,则将 enemy 与 v 归并;同理处置处罚 v 的仇人。
这一步的焦点逻辑是:仇人的仇人是朋侪。通过归并仇人的仇人,尽大概制止同一聚会合出现直接仇人。
初始化并查集,按权值降序排序边。
遍历每条边,查抄是否辩说:
若辩说,输出当前边的权值并竣事。
否则,处置处罚仇人关系,归并干系节点。
若无辩说,输出 0。
准确性证实
贪心准确性:按权值降序处置处罚边,确保初次辩说时的边权是最小大概的答案。
归并逻辑准确性:通过归并仇人的仇人,包管同一聚集内的节点不存在直接敌对关系。若终极仍出现辩说,则阐明无法制止,当前边的权值即为答案。
代码:
#include <bits/stdc++.h>using namespace std;
const int N = 20010;int p[N];
typedef struct e {
int u, v, w;
bool operator < (const e& a) {
return w > a.w;
}
} e;
e a[N*5];int enemy[N];
// 快速读入整数的函数inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
// 查找元素所在聚集的根节点inline int find(int x) {
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
// 归并两个元素所在的聚集inline void merge(int x, int y) {
int px = find(x), py = find(y);
if (px != py) p[px] = py;
}
int n, m;
int main() {
// 利用快读读取 n 和 m
n = read();
m = read();
for (int i = 0; i < m; i++) {
// 利用快读读取每条边的信息
int u = read();
int v = read();
int w = read();
a = {u, v, w};
}
// 初始化并查集
for (int i = 1; i <= n; i++) p = i;
// 按边权从大到小排序
sort(a, a + m);
// 遍历每条边
for (int i = 0; i < m; i++) {
int u = a.u;
int fu = find(u);
int v = a.v;
int fv = find(v);
// 如果两个端点已经在同一聚会合,输出当前边的边权并竣事步伐
if (fu == fv) {
printf("%d\n", a.w);
return 0;
}
// 处置处罚敌对关系
if (!enemy) enemy = v;
else merge(enemy, v);
if (!enemy[v]) enemy[v] = u;
else merge(enemy[v], u);
}
// 若全部边都处置处罚完仍未找到符合条件的边,输出 0
cout << 0 << endl;
return 0;
}
方法二:二分答案+染色法判定二分图
分析:
1. 题目焦点
给定带权无向图,探求最大权值 mid,使得仅保存权值 > mid 的边时,图是二分图。
2. 二分查找的应用
目标:在权值范围 [0, 1e9] 内探求最大 mid。
逻辑:
性子:若 mid 可行(对应子图是二分图),则全部小于 mid 的值也可行。
二分计谋:
初始范围 l=0, r=1e9。
盘算中心值 mid = (l + r + 1) / 2(制止死循环)。
查抄 mid 是否可行:
可行 → 实验更大的 mid(l = mid)。
不可行 → 缩小范围(r = mid - 1)。
3. DFS 验证二分图
目标:判定权值 > mid 的子图是否为二分图。
关键逻辑:
颜色标志:用 color 数组纪录节点颜色(1/2)。
DFS 遍历:
对每个未染色的节点,实验染色。
遍历其毗邻边,若边权 ≤ mid → 跳过。
若毗邻节点已染色且颜色雷同 → 非二分图。
若未染色 → 递归染色为相反颜色。
准确性:DFS 确保连通子图的二分性,遍历全部节点确保全局二分性。
4. 准确性证实
二分查找准确性:
单调性:若 mid 可行,则全部更小值也可行,因此二分可找到最大值。
DFS 验证准确性:
二分图界说:相邻节点颜色差别。DFS 通过递归欺凌相邻节点颜色差别,若辩说则非二分图。
5. 复杂度分析
时间:
二分查找:O(log(1e9)) ≈ 30 次。
每次验证:O(n + m)(DFS 遍历全部节点和边)。
总复杂度:O(30(n + m))。
空间:
颜色数组:O(n)。
6. 关键细节
制止死循环:
盘算 mid 时加 1(mid = (l + r + 1) / 2),确保 l < r 时 mid 准确递增。
颜色数组重置:
每次验证前需重置 color 数组为 0(未染色)。
处置处罚孤立节点:
未染色的节点需单独启动 DFS,确保全部连通分量均为二分图。
7. 常见错误点
边权判定错误:误将 mid ≥ w 作为跳过条件(应 mid ≥ w 时跳过,即保存 w > mid 的边)。
颜色辩说处置处罚:发现颜色雷同节点时需立即返回 false,而非跳过。
二分界限处置处罚:初始 r 设为 1e9 需覆盖全部大概权值,制止遗漏。
8. 总结
方法焦点:利用二分查找的单调性快速缩小范围,团结 DFS 验证每个候选值的可行性。
上风:高效办理带权二分图题目,时间复杂度可控。
实用场景:雷同 “最大 / 最小满足条件的阈值” 题目,且条件可通过二分图验证。
代码:
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
#define x first
#define y second
typedef pair<int, int> pii;
const int N = 20010;
int n, m;
unordered_map<int, vector<pii>> e;
bool color[N];
// 深度优先搜刮函数,判定从节点 u 开始,在权值大于便是 mid 的边构成的子图中是否能举行二分染色
bool dfs(int col, int u, int mid) {
color = col;
for (pii t : e) {
int v = t.x;
int w = t.y;
if (mid > w) continue;
if (color[v]) {
if (color[v] == color) return false;
} else {
if (!dfs(3 - col, v, mid)) return false;
}
}
return true;
}
// 查抄在给定权值 mid 下,整个图是否能构成二分图
bool check(int mid) {
memset(color, 0, sizeof color);
for (int i = 1; i <= n; i++) {
if (color == 0) {
if (!dfs(1, i, mid)) return false;
}
}
return true;
}
int main() {
// 同一利用 scanf 举行输入
scanf("%d %d", &n, &m);
for (int i = 0; i < m; i++) {
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
e.push_back({v, w});
e[v].push_back({u, w});
}
int l = 0, r = 1e9;
while (l < r) {
// 制止二分查找死循环,mid 向上取整
int mid = (l + r + 1) / 2;
if (check(mid)) {
l = mid;
} else {
r = mid - 1;
}
}
// 输出终极效果
cout << l << endl;
return 0;
}
棋盘覆盖(棋盘二分图+最大匹配);
给定一个 NN 行 NN 列的棋盘,已知某些格子克制放置。
求最多能往棋盘上放多少块的长度为 22、宽度为 11 的骨牌,骨牌的界限与格线重合(骨牌占用两个格子),而且恣意两张骨牌都不重叠。
输入格式
第一行包罗两个整数 NN 和 tt,此中 tt 为克制放置的格子的数目。
接下来 tt 行每行包罗两个整数 xx 和 yy,表现位于第 xx 行第 yy 列的格子克制放置,行列数从 11 开始。
输特殊式
输出一个整数,表现效果。
数据范围
1≤N≤1001≤N≤100,
0≤t≤100
分析:
初始化:
标志停滞物位置。
将全部格子的匹配状态初始化为未匹配。
遍历白色格子:
仅处置处罚 i+j 为奇数的白色格子(制止重复处置处罚优劣格子)。
探求增广路径:
对每个白色格子 (x,y),实验向四个方向扩展:
若相邻玄色格子 (a,b) 未被访问过且非停滞物:
标志该格子为已访问。
若该玄色格子未匹配,或其已匹配的白色格子能找到新的匹配路径,则更新匹配关系。
更新匹配数:
每次乐成找到增广路径后,匹配数加一。
4. 算法准确性
二分图性子:优劣格子的相邻关系天然构成二分图,确保匈牙利算法的实用性。
增广路径原理:通过递归回溯,不停调解匹配关系,包管每次匹配数最大化。
5. 复杂度分析
时间复杂度:O(n^2 * n^2),每个白色格子最多实验 n^2 次扩展。
空间复杂度:O(n^2),存储匹配状态和访问标志。
代码:
#include <bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
PII match[N][N];
bool g[N][N], st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool find(int x, int y)
{
for (int i = 0; i < 4; i ++ )
{
int a = x + dx, b = y + dy;
if (a && a <= n && b && b <= n && !g[a] && !st[a])
{
st[a] = true;
PII t = match[a];
if (t.x == -1 || find(t.x, t.y))
{
match[a] = {x, y};
return true;
}
}
}
return false;
}
int main()
{
cin >> n >> m;
while(m--)
{
int x,y;
cin >> x >> y;
g[x][y] = true;
}
memset(match,-1,sizeof match);
int res = 0;
for(int i=1;i<=n;i++)
{
for(int j = 1;j<=n;j++)
{
if((i+j)%2 && !g[j])
{
memset(st,0,sizeof st);
if(find(i,j))res++;
}
}
}
cout << res << endl;
return 0;
}
呆板使命(最小覆盖点==最大匹配边(二分图中))
有两台呆板 A,BA,B 以及 KK 个使命。
呆板 AA 有 NN 种差别的模式(模式 0∼N−10∼N−1),呆板 BB 有 MM 种差别的模式(模式 0∼M−10∼M−1)。
两台呆板最开始都处于模式 00。
每个使命既可以在 AA 上实验,也可以在 BB 上实验。
对于每个使命 ii,给定两个整数 aa 和 bb,表现如果该使命在 AA 上实验,必要设置模式为 aa,如果在 BB 上实验,必要模式为 bb。
使命可以以恣意次序被实验,但每台呆板转换一次模式就要重启一次。
求怎样分配使命并公道安排次序,能使呆板重启次数最少。
输入格式
输入包罗多组测试数据。
每组数据第一行包罗三个整数 N,M,KN,M,K。
接下来 KK 行,每行三个整数 i,ai,a 和 bb,ii 为使命编号,从 00 开始。
当输入一活动 00 时,表现输入克制。
输特殊式
每组数据输出一个整数,表现所需的呆板最少重启次数,每个效果占一行。
数据范围
N,M<100,K<1000N,M<100,K<1000
0≤a<N0≤a<N
0≤b<M
分析:
代码:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m, k;
int match[N];
bool g[N][N], st[N];
bool find(int x)
{
for(int i=0;i<m;i++)
{
if(!st && g[x])//没访问过,且有边
{
st = true;//访问标志
if(match==-1||find(match))//如果没匹配过 or 匹配的可以换人
{
match=x;//标志匹配 这条边,也就是这个使命可以被x这个点覆盖/
return true;
}
}
}
return false;
}
int main()
{
while (cin >> n, n)
{
cin >> m >> k;
memset(g, 0, sizeof g);
memset(match, -1, sizeof match);
while (k -- )
{
int t, a, b;
cin >> t >> a >> b;
if (!a || !b) continue;
g[a] = true;
}
int res = 0;
for (int i = 0; i < n; i ++ )
{
memset(st, 0, sizeof st);
if (find(i)) res ++ ;
}
cout << res << endl;
}
return 0;
}
骑士放置(棋盘二分图+最大独立集):
给定一个 N×MN×M 的棋盘,有一些格子克制放棋子。
问棋盘上最多能放多少个不能相互攻击的骑士(国际象棋的“骑士”,雷同于中国象棋的“马”,按照“日”字攻击,但没有中国象棋“别马腿”的规则)。
输入格式
第一行包罗三个整数 N,M,TN,M,T,此中 TT 表现克制放置的格子的数目。
接下来 TT 行每行包罗两个整数 xx 和 yy,表现位于第 xx 行第 yy 列的格子克制放置,行列数从 11 开始。
输特殊式
输出一个整数表现效果。
数据范围
1≤N,M≤100
分析:
棋盘分别:将 n 行 m 列的棋盘上的格子依据 (i + j) 的奇偶性分别为两个聚集,就像国际象棋棋盘的优劣格那样,从而构建出二分图。
边的界说:若一个格子能通过马走 “日” 的规则到达另一个非停滞物格子,而且这两个格子属于差别的聚集,那么它们之间存在一条边。
输入与初始化
- 输入处置处罚:运用 scanf 读取棋盘的行数 n、列数 m 以及停滞物的数目 k
- 借助 while 循环读取每个停滞物的坐标 (x, y),并把 g[x][y] 标志为 true,以此表明该位置是停滞物。
- 数组初始化:
- g[N][N]:二维布尔数组,用于标志棋盘上每个格子是否为停滞物。
- st[N][N]:二维布尔数组,在探求增广路径时,用于标志某个格子是否已经被访问过,防止重复访问。
- mat[N][N]:二维 pii 范例数组,存储每个格子的匹配信息,mat[j] 表现与格子 (i, j) 匹配的格子坐标。
3. 匈牙利算法探求最大匹配
- find 函数:
- 该函数的作用是为格子 (x, y) 探求一个匹配的格子。
- 遍历马走 “日” 的 8 个方向,盘算新的坐标 (xx, yy)。
- 查抄新坐标是否在棋盘范围内(1 <= xx <= n 且 1 <= yy <= m),是否为停滞物(g[xx][yy]),以及是否已经被访问过(st[xx][yy])。
- 若满足条件,标志该格子为已访问(st[xx][yy] = 1),并获取该格子当前的匹配信息 (px, py)。
- 如果该格子未匹配(px == 0),大概可以通过递归调用 find(px, py) 为其当前匹配的格子找到新的匹配,则更新匹配信息(mat[xx][yy] = {x, y}),并返回 true 表现匹配乐成;否则返回 false。
- 遍历棋盘:
- 在 main 函数中,遍历棋盘上的全部格子 (i, j)。
- 若该格子是停滞物(g[j])大概 i + j 为奇数,则跳过该格子。
- 对于符合条件的格子,调用 find 函数实验匹配,每次匹配乐成则将匹配数 res 加 1。
4. 效果盘算与输出
通过 n * m - tmp - res 盘算未被匹配且非停滞物的格子数目。此中,n * m 是棋盘上格子的总数,tmp 是停滞物的数目,res 是最大匹配数。末了将盘算效果输出。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
const int N=110;
bool st[N][N],g[N][N];
pii mat[N][N];
int n,m,k;
int dy[8]={1,2,2,1,-1,-2,-2,-1};
int dx[8]={2,1,-1,-2,-2,-1,1,2};
int find(int x,int y){
for(int i=0;i<8;i++){
int xx=dx+x,yy=y+dy;
if(xx<1||xx>n||yy<1||yy>m) continue;
if(st[xx][yy]||g[xx][yy])continue;
st[xx][yy]=1;
int px=mat[xx][yy].first;
int py=mat[xx][yy].second;
if(px==0||find(px,py)){
mat[xx][yy]={x,y};
return true;
}
}
return false;
}
int main(){
scanf("%d %d %d",&n,&m,&k);
int tmp=k;
while(k--){
int x,y;
cin>>x>>y;
g[x][y]=true;
}
int res=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(g[j]||(i+j)%2)continue;
memset(st,0,sizeof st);
if(find(i,j))res++;
}
}
cout<<n*m-tmp-res;
}
vani和cl2的捉迷藏(最小重复路径点覆盖)
Vani 和 cl2 在一片树林里捉迷藏。
这片树林里有 NN 座房子,MM 条有向蹊径,构成了一张有向无环图。
树林里的树非常茂密,足以遮挡视线,但是沿着蹊径望去,却是视野开阔。
如果从房子 AA 沿着路走下去可以大概到达 BB,那么在 AA 和 BB 里的人是可以大概相互望见的。
现在 cl2 要在这 NN 座房子里选择 KK 座作为藏身点,同时 Vani 也专挑 cl2 作为藏身点的房子进去探求,为了制止被 Vani 望见,cl2 要求这 KK 个藏身点的恣意两个之间都没有路径相连。
为了让 Vani 更难找到本身,cl2 想知道最多能选出多少个藏身点。
输入格式
输入数据的第一行是两个整数 NN 和 MM。
接下来 MM 行,每行两个整数 x,yx,y,表现一条从 xx 到 yy 的有向蹊径。
输特殊式
输出一个整数,表现最多能选取的藏身点个数。
分析:
团体思绪概述
这段代码的焦点目标是求解一个有向图的最小路径覆盖题目,通过将其转化为二分图最大匹配题目来办理。具体来说,先根据输入构建有向图的毗邻矩阵,再利用 Floyd-Warshall 算法举行通报闭包运算,末了利用匈牙利算法求解二分图的最大匹配数,从而得到最小路径覆盖数。
具体步调分析
1. 数据输入与图的初始化
输入读取:在 main 函数中,起首利用 scanf 读取节点数目 n 和边的数目 m。接着,通过 while 循环读取每条边的出发点 u 和止境 v,并将毗邻矩阵 g[v] 标志为 1,以此构建有向图。
毗邻矩阵 g:g[N][N] 用于存储有向图的边信息,g[j] 为 1 表现存在从节点 i 到节点 j 的有向边,为 0 则表现不存在。
标志数组 st:st[N] 用于在匈牙利算法中标志节点是否已被访问,制止重复访问。
匹配数组 mat:mat[N] 用于纪录每个节点的匹配信息,mat 存储与节点 i 匹配的节点编号。
2. Floyd-Warshall 算法求通报闭包
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++){
g[j] = g[k] || g[k][j];
}
原理:Floyd-Warshall 算法是一种动态规划算法,用于求解图中恣意两点之间的可达性。这里通过三重循环更新毗邻矩阵 g,如果存在从节点 i 到节点 k 的路径,而且存在从节点 k 到节点 j 的路径,那么就以为存在从节点 i 到节点 j 的路径,将 g[j] 标志为 1。
作用:颠末通报闭包运算后,g[j] 为 1 表现从节点 i 可以间接或直接到达节点 j,如允许以将图的可达性信息完备地纪录下来,方便后续的匹配操纵。
3. 匈牙利算法求解二分图最大匹配
find 函数:
该函数的作用是实验为节点 x 探求一个匹配的节点。
遍历全部节点 i,如果节点 i 已经被访问过(st 为 true),则跳过该节点。
标志节点 i 为已访问(st = true),并查抄是否存在从节点 x 到节点 i 的路径(g[x] 为 1)。
如果节点 i 未匹配(mat 为 0),大概可以通过递归调用 find(mat) 为其当前匹配的节点找到新的匹配,则更新匹配信息(mat = x),并返回 true 表现匹配乐成;否则返回 false。
遍历节点探求匹配:在 main 函数中,遍历全部节点 i,每次调用 find 函数前将 st 数组清零,确保每次探求匹配时标志信息是干净的。如果 find(i) 返回 true,阐明为节点 i 找到了匹配的节点,将匹配数 res 加 1。
4. 盘算最小路径覆盖数
在有向无环图中,最小路径覆盖数便是节点数减去二分图的最大匹配数。这里终极输出的 res 即为二分图的最大匹配数,而最小路径覆盖数在本题代码中虽未单独盘算输出,但根据原理可知最小路径覆盖数为 n - res。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=210;
bool g[N][N],st[N];
int n,m;
int mat[N];
bool find(int x){
for(int i=1;i<=n;i++){
if(!g[x])continue;
if(i==x)continue;
if(st)continue;
st=true;//标志思量过
if(!mat||find(mat)){//没对象 or 可以再找一个
mat=x;
return true;//返回
}
}
return false;
}
int main(){
scanf("%d %d",&n,&m);
while(m--){
int u,v;
scanf("%d%d",&u,&v);
g[v]=1;//读入边
}
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
g[j]=g[j]||(g[k]&&g[k][j]);//通报闭包
}
int res=0;
for(int i=1;i<=n;i++){//思量每个点
memset(st,0,sizeof st);
if(find(i))res++;//
}
cout<<n-res;
}
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |