题解 SP11579 【COMPANYS - Two Famous Companies】
stone_juice石汁 · · 题解
本题第一篇题解QAQ
题目大意:
给出一个图,图中有
要求:求这张图的最小生成树,且满足刚好有
输入:
输入有多个测试数据,对于每个测试数据:
第一行输入三个数:
接下来的m行 每行分别四个数:
貌似没给数据组数,但是目测不超过
题解:
最开始我以为这是一道裸的最小生成树,然后我写一半发现不对...
正确做法:二分答案 + 最小生成树
不了解最小生成树的最好先去了解一下
-
我写的
Blog --Kruskal 算法 -
模板题
进入正题:
我最开始是这么想的:
不是要满足白边条数吗?我先把白边给连到目标个数k ,之后再连黑边。
我用的
然后我被狠狠地打脸了
为什么这样不行?我们来看看这张图。
如果你和我最开始思路一样,先选白边,那么权值为
这样选无疑没有白边选
所以刚刚那个解法是个错解。
正解:
我们不是要求最小生成树吗?
那就求呗。
你说有白边的限制??我先不去管它就行了。
用
我们用一个计数器,记录我们跑这中英混杂也是没谁了)
那么我们将会面临两种情况:
-
Ⅰ、
k > wg ,这个时候说明白边选少了。 -
Ⅱ、
k < wg , 恰恰相反,这个时候说明白边选多了。
对于情况Ⅰ,选少了必然是因为白边权值整体过大引起的,导致一部分黑边排在了前面,抢占了白边的位置。
而对于情况Ⅱ,选多了必然是因为白边权值整体过小引起的,导致一部分过多的白边涌进了数组前方而被选中。
怎么处理这两种情况?
鲁迅说的好:你权值太小我给你拉上来,你权值过大我给你打下去。
如果白边整体权值太小,我们就把所有白边的权值加上一个正值,让整体权值变大。
反之,白边整体权值过大,我们就把所有白边的权值加上一个负值。让整体权值变小。
我们把加上的这个值暂叫做偏移量,用
处理完之后再跑一边
那么我们得到这个最小生成树之后,计算一遍它的总权值。*但是要注意的是:我们每条白边的权值都加上了一个偏移量,所以要对结果减去 ($k mid$)才符合要求。**
有人看到这会问了:
- 怎么求这个
mid
那么今天的压轴嘉宾来了:二分!
当然,有了
显然,
当
为什么我敢说这个
因为任一一条边的权值都不大于
最极端的两种情况就是白边都为
所以这个
这样一来,大部分的东西就处理完了。
最后提一些小细节吧:
-
1、
mid 可能会出现这种情况:偏移值为mid 时,wg > k ,mid 变大后,wg < k 。这....真是左右为难。事实上,题目是保证有解的,如果出现这种情况,代表着必定有黑边的权值与白边相等。我们只需要替换一下就行了。(白边少了,把黑边替换成白边)反正权值相等,替换也不改变结果
-
2、由于我们每次改变
mid 后都要跑一边kruskal ,所以某些数据是需要清空的(比如wg )不清空小心数据叠加爆零啊QAQ -
3、由于我们每次都会给所有白边加上
mid ,所以在跑完kruskal 后要把所有白边恢复原状,也就是都减去mid 。 要善始善终~ -
4、数据居然会有编号为
0 的点??所以我直接把每个点的编号加1 了
大概就是这样了,上代码!!
#include<bits/stdc++.h>
#define mian main
#define QWQ puts("QWQ");
using namespace std;
struct node
{
int u, v, w, x;//u,v,w,x:连边,被连边,权值,颜色
}e[100005];
int n, m, k, t;
int ans, sum, wg, g;
//ans:答案,sum:每次跑kruskal记录的最小权值和,wg不解释,g:记录跑kruskal时连了多少边,方便退出
int f[50005];
int _find(int x)//并查集
{
while(x != f[x]) x = f[x] = f[f[x]];
return x;
}
void clean()//清空子函数
{
g = wg = sum = 0;
for(int i = 1; i <= n; i ++)f[i] = i;
}
bool cmp(node a, node b)
{
if(a.w == b.w)return a.x < b.x;//权值相同,白边优先
return a.w < b.w;//权值不同,权值小在前
}
void kruskal()//最小生成树板子
{
sort(e + 1, e + 1 + m, cmp);
for(int i = 1; i <= m; i ++)
{
int fu = _find(e[i].u);
int fv = _find(e[i].v);
if(fu == fv) continue;//判环
f[fu] = fv;
g ++;//增加总数
if(e[i].x == 0) wg ++;//增加白边数
sum += e[i].w;//每次计算权值和
if(g >= n - 1) break;//达到规定边数退出
}
}
int main()
{
while(~scanf("%d%d%d", &n, &m, &k))//循环读入
{
t ++;//记录数据组数
for(int i = 1; i <= m; i ++)
{
scanf("%d%d%d%d", &e[i].u, &e[i].v, &e[i].w, &e[i].x);
e[i].u ++;
e[i].v ++;//居然会有编号为0的点,惊了啊QAQ,点的序号要加上1
}
int l = -150, r = 150, mid;
while(l <= r)//二分开始
{
mid = (l + r) / 2;//计算中间数,也是加上的偏移量
for(int i = 1; i <= m; i ++)
if(e[i].x == 0) e[i].w += mid;//每个白边权值加上偏移量
clean();//每次执行前清除
kruskal();//跑最小生成树
for(int i = 1; i <= m; i ++)
if(e[i].x == 0) e[i].w -= mid;//白边权值减回来
if(k > wg) r = mid - 1;
else l = mid + 1, ans = sum - k * mid;
//计算ans值。因为k条白边都加了一个mid,所以计算时要减回来
//顺带判断改变l,r值以此改变mid值
}
printf("Case %d: %d\n", t, ans);
}
return 0;
}
顺带一提,这道题和本题完全一样...就是输入输出略有不同。双倍经验
我也不知道有没有把你们讲懂QAQ,如果没看懂可以去那边看看,希望对你们有帮助。