星星之火

BZOJ4479 [JSOI2013] 吃货jyy 解题报告(三进制状态压缩+欧拉回路)

题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=4479

Description

【故事背景】
作为JSOI的著名吃货,JYY的理想之一就是吃遍全世界的美食。要走遍全世界当然需要不断的坐飞机了。而不同的航班上所提供的餐食是很不一样的:比如中国的航班会提供中餐,英国的航班有奶茶和蛋糕,澳大利亚的航班有海鲜,新加坡的航班会有冰激凌……JYY选出了一些他特别希望品尝餐食的航班,希望制定一个花费最少的旅游计划,能够从南京出发,乘坐所有这些航班并最后回到南京。

【问题描述】

世界上一共有N个JYY愿意去的城市,分别从1编号到N。JYY选出了K个他一定要乘坐的航班。除此之外,还有M个JYY没有特别的偏好,可以乘坐也可以不乘坐的航班。一个航班我们用一个三元组(x,y,z)来表示,意义是这趟航班连接城市x和y,并且机票费用是z。每个航班都是往返的,所以JYY花费z的钱,既可以选择从x飞往y,也可以选择从y飞往x。南京的编号是1,现在JYY打算从南京出发,乘坐所有K个航班,并且最后回到南京,请你帮他求出最小的花费。

Input

输入数据的第一行包含两个整数N和K;
接下来K行,每行三个整数x,y,z描述必须乘坐的航班的信息,数据保证
在这K个航班中,不会有两个不同的航班在同一对城市之间执飞;
第K+2行包含一个整数M;
接下来M行,每行三个整数x,y,z描述可以乘坐也可以不乘坐的航班信息。
2<=N<=13,0<=K<=78,2<=M<=200,1<=x,y<=N,1<=z<=10^4

Output

输出一行一个整数,表示最少的花费。数据保证一定存在满足JYY要求的
旅行方案。

Sample Input

6 3
1 2 1000
2 3 1000
4 5 500
2
1 4 300
3 5 300

Sample Output

3100
一个可行的最佳方案为1->2->3->5->4->1。
机票所需的费用为1000+1000+300+500+300=3100
 

题目大意,给一张有权无向图,标记其中的某些边。从点1出发要求我们找一条回路,且这条回路经过所有的标记边,最小化消耗的费用

考虑回路的性质,因为是在无向图中,只要我们的边集里的每个点度数都是偶数,那么就一定有一种方法形成回路

于是我们就枚举一个联通子图,注意到n的值很小,313≈1600000。咳咳,这是完全没有问题的。然后我们考虑从结点1开始向点集里面加点。状态压缩的时候,我们定义0表示当前点不在点集里,1表示在点集里且度数为奇数,2表示在点集里且度数是偶数。加点的时候我们有两种情况:

1.这个点是一条必须边的端点,且另一个端点在点集里,那么这条边我们一定要加进去。但注意我们此时并不用添加代价,所有必须边的代价放到最后加入(代价同时也包括点的度数)

2.枚举点集中的点,这个点与枚举的点存在一条路径,我们取最短路添加进去就好(最短路在之前用floyd先预处理好),同时我们要算上最短路的代价,改变点度数的奇偶性。值得注意的是,读者可能会有以下两个疑惑:

  ①要是最短路路径上的点不在我们枚举的点集里面怎么办?首先我们要明确,在连上最短路的时候,路径上的点奇偶性是不会改变的。其次,就算有些点不在点集里,我们要的只是当前这个状态,换言之就是这个结        果,不需要考虑中间的过程。而在之后的操作里我们也发现还需要的代价值取决于度数为奇数的点,路径上的点不会有影响。

  ②要是最短路经过了点集里的必须边呢?我们是不是多算了代价?这些边的端点的奇偶性是不是也变了?其实都没有,其实这道题说是欧拉回路的话我们的定义还是存在偏差,可以理解为我们重复经过了这个点,或者说多连了两条边在上面,奇偶性没有影响。再者航班是来回几次就要付出多少倍的钱的。读者模拟几张图发现出现上述情况的话我们的回路都必须绕回这个点,因此最短路付出的代价是必要的。

       

接下来我们其实就得到每一个状态形成的最小代价,但是这样还不行,我们还要让每一个点的度数都是偶数。于是我们必须把奇数点两两匹配,这个的代价我们也可以用g数组预处理好。(若是出现疑问可以参考上面的①②点),当然别忘了我们还有必须边的代价没有计算,此时一起算上去,ans取min值就好了。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;

const int maxn=15;
const int mxn=78+5;
const int inf=0x3f3f3f3f;
int n,m,tot,ans;
int head[maxn],bin[maxn],pin[maxn],g[1<<maxn],dist[maxn][maxn],f[1600000],deg[maxn],a[maxn];
struct EDGE
{
    int to;int next;int l;
}edge[mxn<<1];
inline int read()
{
    char ch=getchar();
    int s=0,f=1;
    while (!(ch>='0'&&ch<='9')) {if (ch=='-') f=-1;ch=getchar();}
    while (ch>='0'&&ch<='9') {s=(s<<3)+(s<<1)+ch-'0';ch=getchar();}
    return s*f;
}
void init()
{
    memset(g,inf,sizeof(g));
    memset(dist,inf,sizeof(dist));
    memset(f,inf,sizeof(f));
    for (int i=1;i<=n;i++) dist[i][i]=0;
    bin[0]=pin[0]=1;
    for (int i=1;i<=n;i++) bin[i]=bin[i-1]*2,pin[i]=pin[i-1]*3; 
}
void add(int x,int y,int l)
{
    edge[++tot]=(EDGE){y,head[x],l};
    head[x]=tot;
    edge[++tot]=(EDGE){x,head[y],l};
    head[y]=tot; 
}
void floyd()//最短路 
{
    for (int k=1;k<=n;k++)
        for (int i=1;i<=n;i++)
            for (int j=1;j<=n;j++)
            dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
}
void pre_dp()//需处理奇数点匹配的代价 
{
    g[0]=0;
    for (int i=0;i<bin[n];i++)
        for (int j=1;j<=n;j++)
            if (!(i&bin[j-1]))
                for (int k=j+1;k<=n;k++)
                    if (!(i&bin[k-1])) 
                        g[i^bin[j-1]^bin[k-1]]=min(g[i^bin[j-1]^bin[k-1]],g[i]+dist[j][k]);
}
void dp()//枚举状态,并转移 
{
    queue <int> q;
    f[2]=0;q.push(2);
    while (!q.empty())
    {
        int s=q.front();q.pop();
        int cnt=0;
        for (int i=1;i<=n;i++) if (s/pin[i-1]%3>0) a[++cnt]=i;
        for (int i=1;i<=n;i++)
            if (s/pin[i-1]%3==0)//注意三进制状态压缩的方法 
            {
                for (int j=head[i];j;j=edge[j].next)
                    if (s/pin[edge[j].to-1]%3>0)
                    {
                        int s1=s+pin[i-1]*2;//必须点的度数一开始不算,视为0 
                        if (f[s]>=f[s1]) continue;
                        if (f[s1]==inf) q.push(s1);
                        f[s1]=f[s];//更新 
                    }
                for (int j=1;j<=cnt;j++)
                {
                    int s1=s+pin[i-1];
                    s1+=(s/pin[a[j]-1]%3==1)?pin[a[j]-1]:-pin[a[j]-1];//改变度数 
                    if (f[s]+dist[i][a[j]]>=f[s1]) continue;
                    if (f[s1]==inf) q.push(s1);
                    f[s1]=f[s]+dist[i][a[j]];
                }
            }
    }
}
void calc()//统计答案 
{
    ans=inf;
    for (int s=0;s<=pin[n]-1;s++)
    {
        int flag=0;
        for (int i=1;i<=n;i++) if (deg[i]&&s/pin[i-1]%3==0) {flag=1;break;}//如果就必须点没加就直接跳过这个状态 
        if (flag) continue; 
        int now=s;
        for (int i=1;i<=n;i++) if (deg[i]&1) now+=(s/pin[i-1]%3==1)?pin[i-1]:-pin[i-1];//考虑必须边对必须点的度数影响 
        int s1=0;
        for (int i=1;i<=n;i++) if (now/pin[i-1]%3==1) s1^=bin[i-1];
        ans=min(ans,f[s]+g[s1]);
    }
    for (int i=1;i<=tot;i+=2) ans+=edge[i].l; 
}
int main()
{
    n=read();m=read();
    init();
    while (m--)
    {
        int x=read(),y=read(),l=read();
        dist[x][y]=dist[y][x]=min(dist[x][y],l);
        deg[x]++;deg[y]++;
        add(x,y,l);
    }    
    m=read();
    while (m--)
    {
        int x=read(),y=read(),l=read();
        dist[x][y]=dist[y][x]=min(dist[x][y],l);    
    }
    floyd();
    pre_dp();
    dp();
    calc();
    printf("%d\n",ans);
    return 0;
}

 

posted @ 2018-07-11 18:24  星星之火OIer  阅读(476)  评论(0编辑  收藏  举报