洛谷P1525 关押罪犯 题解
前言
题目大意
\(N\) 个罪犯,如果两名罪犯被关押到了一座监狱,那么就会产生一定的怨气值。现在有两座监狱,求一种分配监狱方法,使得最大的怨气值最小。
题意分析
时间紧迫者略
由题意得,怨气值有大有小,求怨气值之和最小,最先想到的就是贪心。
贪心策略最简单的莫过于,按照怨气值从大到小排序,在把怨气值大的一对罪犯分开放的前提下,其他能分开放就分开放,不能分开放就累加怨气值。
然而,贪心的策略显然有些问题。
比如 \(x\) 和 \(y\) 的怨气值为 \(5\),其他还有 \(6\) 个罪犯,都只分别和 \(x\) 和 \(y\) 之间有 \(1\) 的怨气值,且这 \(6\) 个罪犯之间没有怨气值。
按照最开始的贪心策略,\(x\) 和 \(y\) 分开放在两个监狱,其他 \(6\) 个罪犯因此不管放在哪个监狱,都会和 \(x\) 或 \(y\) 产生 \(6\) 的怨气值。最后答案会输出 \(6\)。
手玩数据发现,如果 舍小家为大家 把 \(x\) 和 \(y\) 放在一个监狱,其他 \(6\) 个罪犯都放在另一个监狱,那么最后正确答案应该是 \(5\)。
所以贪心是错的吗?
显然不是。只是题目不只这么简单。
博主太菜,下面分两种方法解该题目,分别是 并查集 和 二分图
并查集
我们不必太在乎每个罪犯具体在哪个监狱,毕竟题目没有要求,只需要知道哪些罪犯属于一个集合即可。
并查集恰恰好可以解决我们的需求。我们拿一个额外的数组 \(b[]\),来记录每一个罪犯的敌人,且是不在一个集合内的敌人。
按照贪心思想,将怨气值从大到小排序,枚举每一对怨气值,如果产生怨气值的两位罪犯 \(x\) 和 \(y\) 属于一个集合内,由于怨气值已从大到小排序且题目要求最大的怨气值,那么直接输出答案即可。
而如果 \(x\) 和 \(y\) 不属于同一个集合内,那么将 \(y\) 与 \(b[x]\) 放在一个集合内, \(x\) 与 \(b[y]\) 放在一个集合内。
随后按照上面步骤操作即可。
代码:
#include<bits/stdc++.h>
#include<cctype>
#define in(a) a = read()
#define Min(a, b) a < b ? a : b
#define Max(a, b) a > b ? a : b
#define out(a) write(a)
#define outn(a) out(a),putchar('\n')
#define ll long long
#define rg register
#define New int
using namespace std;
inline New read()
{
New X = 0,w = 0;
char ch = 0;
while(!isdigit(ch))
{
w |= ch == '-';
ch=getchar();
}
while(isdigit(ch))
{
X = (X << 3) + (X << 1) + (ch ^ 48);
ch = getchar();
}
return w ? -X : X;
}
inline void write(New x)
{
if(x < 0) putchar('-'),x = -x;
if(x > 9) write(x/10);
putchar(x % 10 + '0');
}
const int N = 20000 + 10;
const int M = 100000 + 10;
int a[N], b[N];
struct Prison//存怨气值
{
int x,y;//产生怨气值的两个罪犯
int dan;//怨气值
} f[MAXM];
int n, m;
int q, p, z;
inline bool cmp(Prison a,Prison n)//快排
{
return a.dan > n.dan;
}
inline int find(int num)//并查集
{
if(a[num] == num)
return num;
return a[num] = find(a[num]);
}
inline bool check(int x,int y)//判断是否在一个集合内
{
return find(x) == find(y);
}
inline void unionn(int x,int y)//合并
{
a[find(a[x])] = find(a[y]);
return;
}
int main()
{
in(n), in(m);
for(rg int i = 1; i <= n; ++i)
a[i] = i;
for(rg int i = 1; i <= m; ++i)
in(f[i].x), in(f[i].y), in(f[i].dan);
sort(f+1, f+1+m, cmp);//快排
for(rg int i = 1; i <= m; ++i)
{
q = f[i].x,p = f[i].y,z = f[i].dan;
if(check(p,q))//如果在一个集合内
{
outn(z);
return 0;
}
//不在一个集合内
if(!b[q])
b[q] = p;
else unionn(b[q],p);
if(!b[p])
b[p] = q;
else unionn(b[p],q);
}
puts("0");
return 0;
}
二分图
这里二分图不是匹配,而是判定。二分图判定普遍使用染色法。
题目中求“最大值最小”,不由自主地想到 二分
将罪犯看作点,而二分图的两个集合就是监狱。
罪犯之间的怨气值,不难看出就是边的权值了。
还是先快排,由于二分,需要 从小到大排序 ,然后二分出一个怨气值 \(mid\),枚举所有边,如果这条边的权值小于等于 \(mid\),也就是不会对二分的答案造成影响,则不需要操作。否则进行染色法操作。
如果染色失败,出现矛盾,那么说明二分的答案 \(mid\) 小了,反之则大了,移动左右边界即可。
博主写完后才发现 这里有一个技巧 ,普遍 \(OIer\) 都是左边界取 \(0\),有边界取 \(inf\),然而最大怨气值不会超过排序后边集最后一条边的边权,且最终答案要么是 \(0\) (左边界),要么是其中一条边边权。
故从 二分怨气值 转换为 二分边的编号 。博主没有优化,太懒了
代码
#include<bits/stdc++.h>
#include<cctype>
#define in(a) a = read()
#define out(a) write(a)
#define outn(a) out(a),putchar('\n')
#define ll long long
#define rg register
#define New int
using namespace std;
inline New read()
{
New X = 0,w = 0;
char ch = 0;
while(!isdigit(ch))
{
w |= ch == '-';
ch=getchar();
}
while(isdigit(ch))
{
X = (X << 3) + (X << 1) + (ch ^ 48);
ch = getchar();
}
return w ? -X : X;
}
inline void write(New x)
{
if(x < 0) putchar('-'),x = -x;
if(x > 9) write(x/10);
putchar(x % 10 + '0');
}
const int N = 200000 + 2;
struct Node
{
int nxt, val;
};
int n, m, l, r;
vector<Node>nei[N];//用邻接矩阵存图
inline bool work(int mid)//染色法判定
{
queue <int> q;
int color[20009] = {0};
for(rg int i = 1; i <= n; ++i)
{
if(color[i])
continue;
q.push(i);
color[i] = 1;
while(!q.empty())
{
int x = q.front();
q.pop();
int len = nei[x].size();
for(rg int j = 0; j < len; ++j)//枚举邻居
{
int nxt = nei[x][j].nxt;
int val = nei[x][j].val;
if(val < mid)//边权不影响答案
continue;
//边权影响答案
if(color[nxt] == color[x])//染色法矛盾
return false;
if(color[nxt] == 0)//如果该点未被染色
q.push(nxt);//加入队列
color[nxt] = 3 - color[x];//这里很有技巧,感性理解
}
}
}
return true;
}
int main()
{
in(n), in(m);
for(rg int i = 1; i <= m; ++i)
{
int x = read(), y = read(), z = read();
r = max(r, z);
nei[x].push_back((Node){y, z});//存边
nei[y].push_back((Node){x, z});
}
++r;
while(l+1 < r)
{
int mid = (l+r) >> 1;
if(work(mid))
r = mid;
else l = mid;
}
outn(l);
return 0;
}