【随机化贪心】【Kruskal】【NOI2005】小H的聚会

【任务描述】
小 H 从小就非常喜欢计算机,上了中学以后,他更是迷上了计算机编程。经过多年的不懈努力,小 H 幸运的被选入信息学竞赛省队,就要去他日思夜想的河南郑州参加第 22 届全国信息学奥林匹克竞赛(NOI2005)。
小 H 的好朋友小 Y 和小 Z 得知了这个消息,都由衷的为他感到高兴。
他们准备举办一个 party,邀请小 H 和他的所有朋友参加,为小 H 庆祝一下。
经过好几天的调查, Y 和小 Z 列出了一个小 H 所有好友的名单,上面一共有 N 个人(方便起见,我们将他们编号为 1 至 N 的整数)。然而名单上的人实在是太多了,而且其中不少人小 Y 和小 Z 并不认识。如何把他们都组织起来参加聚会呢?
小 Y 和小 Z 希望为小 H 的 N 个好友设计一张联系的网络,这样,若某个人得知了关于聚会的最新情况,则其他人都可以直接或间接得到消息。同时为了尽量的保证消息传递得简单、高效以及最重要的一点:保密(为了给小 H 一个惊喜,在 party 的筹备阶段这个聚会的消息是绝对不能让他知道的),小 Y 和小 Z决定让尽量少的好友直接联系:
为了保证 N 个好友都能互相直接或间接联系到,只需要让(N-1)对好友直接联系就可以了。

《【随机化贪心】【Kruskal】【NOI2005】小H的聚会》

显然,名单上的好友也不都互相认识,而即使是两个互相认识的人,他们之间的熟悉程度也是有区别的。因此小 Y 和小 Z 又根据调查的结果,列出了一个好友间的关系表,表中标明了哪些人是可以直接联系的,而对于每一对可以互相联系的好友,小 Y 和小 Z 又为他们标出了联系的愉快程度。如 3 和 4 的关系非常好,因此标记他们之间的联系愉快程度为 10;而 1 和 3 是一般的朋友,则他们的愉快程度要小一些。上面的图 1 表示一个 N=5 的联系表,其中点表示名单上的好友,边则表示两个好友可以直接联系,边上的数字即为他们联系的愉快程度。
小 Y 和小 Z 希望大家都能喜欢这次聚会,因此决定在尽量最大化联系网络的愉快程度:所谓联系网络的愉快程度,即每一对直接联系人之间的愉快程度之和。如在图 1 中,加粗的边表示了一个让愉快程度最大联系的网络,其愉快程度为 5+6+10+5=26。
然而,如果让某个人直接和很多的人联系,这势必会给他增添很大的负担。
因此小 Y 和小 Z 还为每个人分别设定了一个最大的直接联系人数 ki,表示在联系网络中,最多只能有 ki 个人和 i 直接联系。
还是用图 1 的例子,若我们为 1 至 5 每个点分别加上了 ki = 1, 1, 4, 2, 2 的限制,则上述方案就不能满足要求了。此时的最优方案如图 2 所示,其愉快程度为3+6+10+5 = 24。

《【随机化贪心】【Kruskal】【NOI2005】小H的聚会》

你能帮小 Y 和小 Z 求出在满足限制条件的前提下,愉快程度尽量大的一个联系网络吗?
【输入格式】
输入文件 party1.in 到 party10.in 已经放在用户目录中。
每个输入文件的第 1 行都是两个整数 N 和 M。N 表示小 H 的好友总数,M 表示小 Y 和小 Z 列出来的可以直接联系的好友对数。
输入文件的第 2 行包含 N 个在[1, N-1]范围内的整数,依次描述 k1, k2, ..., kN。相邻的两个数字之间用一个空格隔开。
以下 M 行,每行描述一对可以互相联系的好友,格式为 ui vi ci。表示 ui 和 vi可以直接联系,他们的联系愉快程度为 ci。
另外,在所有这些数据的最后还有单独的一行包括一个(0, 1]范围内的实数 d作为评分系数。
你的程序并不需要去理会这个参数,但你可以根据这个参数的提示去设计不同的算法。有关 d 的说明,可以参见后面的评分方法。
【输出格式】
本题是一道提交答案式的题目,你需要提供十个输出文件从 party1.out 到party10.out。
每个文件的第 1 行为一个整数,表示你找到的最大的愉快程度。
以下(N-1)行,描述这个网络。每行一个数 ei,表示在网络中,让输入文件中第(ei + 2)行描述的一对好友直接联系。
【输入样例】
5 6
1 1 4 2 2
1 2 5
1 3 3
2 3 6
2 5 3
3 4 10
4 5 5
0.00001
【输出样例】
24
2
3
5
6
【样例说明】
详见任务描述中的例子。
【评分方法】
本题设有部分分,对于每一个测试点:
 如果你的输出方案不合法,即 ei 不符合范围或 ei 有重复或网络不连通等,该测试点得 0 分。
 如果你输出的方案和输出文件第 1 行的愉快程度不一致,该测试点得 0分。
 否则该测试点得分按如下方法计算:设
a = (1 − d ) * our _ ans
b = (1 + d * 0.5) * our _ ans
u 如果你的结果小于 a,该测试点得 0 分;
u 如果你的结果大于 b,该测试点得 15 分;
u 否则你的得分为

《【随机化贪心】【Kruskal】【NOI2005】小H的聚会》

其中的 d 为评分系数(输入数据中最后一行的实数),our_ans 为我们提供的参考解答,your_ans 为你的答案。
【你如何测试自己的输出】
我们提供 party_check 这个工具作为测试你的输出文件的办法。
使用这个工具的方法是在控制台中输入:
./party_check <测试点编号 X>
在你调用这个程序后,party_check 将根据输入文件 partyX.in 和你的输出文件partyX.out 给出测试的结果,其中包括:
 Error: Not connected:你的程序输出的联系网络不连通;
 Error: Edge xxx is duplicated:第 xxx 条边被输出了两次;
 Error: Edge in Line xxx is out of range:你的程序在第 xxx 行输出的边的编号不在[1, M]范围内;
 Error: Degree of Friend xxx is out of range:在联系网络中,和编号为 xxx的好友直接联系的人超过了限制;
 Error: Scheme & happiness mismatch:方案和第一行的愉快程度不一致;
 测试程序非法退出:其他情况;
Correct! Happiness = xxx:输出正确。

前三个数据十分简单,没有度的限制(因为每个点的限制都是n – 1),直接用Kruskal算法求解即可。

而对于4~6的数据,十分阴险,在众多的999中藏了一个30!
用爬山法可以将这三组数据做全部做到15分。
方法是先将边表按权值从大到小排序,每次随机交换其中两个数,若得到的新解(也采用Kruskal算法求解)比当前解更优,则接受新解,否则以极小的概率接受它。

对于7~10的数据,可以用模拟退火算法解决。
大致同爬山法,只是要维护一个温度,若每次生成的新解小于当前解(差值为delta),那么以exp(delta / tem)的概率接受较差解。
代码:

/***********************************\
 * @prob: NOI2005 party            *
 * @auth: Wang Junji               *
 * @stat: Accepted.(117分)         *
 * @date: June. 5th, 2012          *
 * @memo: 随机化贪心、Kruskal算法    *
\***********************************/
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <string>
#include <ctime>
#include <cmath>

const int maxN = 1010, maxM = 20010, INF = 0x3f3f3f3f;
struct Edge
{
    int u, v, d, ord; Edge() {}
    Edge(int u, int v, int d, int ord): u(u), v(v), d(d), ord(ord) {}
} edge[maxM]; bool used[maxM]; int deg[maxN], Deg[maxN], F[maxN], n, m, ans;

inline int cmp(const Edge &a, const Edge &b) {return a.d > b.d;}

int Find(int x) {return F[x] == x ? x : F[x] = Find(F[x]);}

inline int calc()
{
    memcpy(deg, Deg, sizeof deg);
    memset(used, 0, sizeof used);
    int cnt = 0, ans = 0;
    for (int i = 1; i < n + 1; ++i) F[i] = i;
    for (int i = 0; i < m; ++i)
    {
        int u = edge[i].u, v = edge[i].v, d = edge[i].d;
        if (Find(u) != Find(v) && deg[u] && deg[v])
            ++cnt, ans += d, used[edge[i].ord] = 1, F[F[u]] = F[v], --deg[u], --deg[v];
    }
    if (cnt < n - 1) return ~INF; else return ans;
}

inline double _rand() {return (double) rand() / RAND_MAX;}

inline void SAA()
{
    int ths = calc();
//    for (double tem = 6e2; tem > 1e-7; tem *= 0.999996) /*模拟退火用*/
    for (int t = 0; t < 100000; ++t) /*爬山法用*/
    {
        for (int k = 0; k < 1; ++k)
        {
            int x = rand() % m, y = rand() % m;
            while (x == y) x = rand() % m;
            std::swap(edge[x], edge[y]);
            int delta = calc() - ths;
            if (delta > 0)
            {
                ths += delta;
                if (ths > ans)
                {
                    ans = ths;
                    
                    FILE *tmp = fopen("../out/party6.out", "w");
                    fprintf(tmp, "%d\n", ans);
                    for (int i = 0; i < m; ++i)
                        if (used[i]) fprintf(tmp, "%d\n", i + 1);
                    fclose(tmp);
                }
            }
//            else if (_rand() < exp(delta / tem)) ths += delta; /*模拟退火用*/
            else if (_rand() * 6000000 < 100) ths += delta; /*爬山法用*/
            else std::swap(edge[x], edge[y]);
        }
    }
    return;
}

int main()
{
    freopen("../in/party6.in", "r", stdin);
    srand(time(NULL)); scanf("%d%d", &n, &m);
    for (int i = 1; i < n + 1; ++i) scanf("%d", Deg + i);
    for (int i = 0, u, v, d; i < m; ++i)
        scanf("%d%d%d", &u, &v, &d), edge[i] = Edge(u, v, d, i);
    std::sort(edge, edge + m, cmp); SAA();
    return 0;
}

点赞