HDU多校联赛4-AND Minimum Spanning Tree(位运算)

题目链接:
http://acm.hdu.edu.cn/showproblem.php?pid=6614
AND Minimum Spanning Tree

大致题意:
给你一个n,代表1到n,n个点,每两点间都有边,边的权值为两端点的按位&操作结果,让你计算连通所有点的最小权值之和,并且输出2到n都连接谁。

题解:
看下输入,第一行代表的多组测试的组数,然后每组一行代表1到n个节点;输出,每组第一行是最小权值之和,第二行是Fi(2<=i<=n),代表的是i结点和谁相连,这是个坑,不容易理解。
自己看到题目有MST,于是最先开始就用最小生成树的方法,我先试了Prim,发现只能算出最小的权值,并不能算出Fi,然后又试的平时不会的Kruskal,发现也改不好,于是乎几小时过去…就放弃了。
最后,听过学长的讲解,瞬间听懂,压根就没有用MST的算法!就是根据&找规律。

正式开始题解:

首先看一下按位&运算示意图:
在这里插入图片描述
总结:有0则0,无0则1

其次,我们发现结点之间有着非常奇妙的关系:

提示:下面中提到的按位&操作的最优解就是两点之间的权值

1.偶数
如果该数是偶数的话,那么它的二进制最后一位必定是0,因为二进制最后一位代表是2^0,也即是1,因此根据&运算的操作特性,找到最后一位是1,并且字典序最小的数,就能保证这条边的权值为0,然后连接即可。
那么怎么找呢?这时我们发现,二进制最后一位是1,并且字典序最小的数就是1!,那么也就是说只要是偶数,让它连接1就行,并且权值都是0

2.奇数
奇数的话最后 一位必定是1了,但是还是会出现两种情况,一种是二进制都由1组成,如数字7(111)一种是二进制中间有0,如数字21(10101),至于为什么要分这两种情况接下来会讲到。

2.1 全1(二进制)奇数
对于这种情况,按照&操作来讲,最优解当然是 0但是对于全1的奇数来讲,如何找到一个二进制位最后一位是0的数呢? 最快速得到的一个数就是+1后的数,如7(111)+,找到的数就是8(1000),7&8的结果就是0,当然辣,如果这个8在1到n的范围内,就连接它就行了,但是如果不在这个范围内呢? 那就找不到 0 这个最优解了,那现在就找次优解 1 ,那么按照字典序来讲,让它连接数字1就好了!

2.1 中间有0(二进制)奇数
还是老思路,先找有没有能和它按位&一下结果得到0的,然后我们可以发现,将二进制一直右移,直到某个0变成二进制的最后一位,那么此时的得到的数,与原数按位&得到的就是0了。

比如21(10101),二进制右移一位,变成10(1010),21&10=0。
似乎到这里就结束了了,但是 仔细想想21(10101)明明可以和2(10)按位&啊!
这时我们发现,虽然结果都是0,但是2比10的字典序更靠前。
但是我们仔细想想,2不就是21(10101)从左往右数第一个0所在位的权值吗?
因此,中间有0(二进制)奇数只需要找到上述所说的数就行了。

下面是寻找二进制中从左往右数第一个0所在位的权值的函数

ll judge(ll a)
{
    ///返回a从右边数第一个0的权值
    ll res=1;
    while(a)///逐位遍历
    {
        if((a&1)==0)///当前最后一位是0
        {
            return res;///返回该位的权值
        }
        ///当前最后一位不是0
        res<<=1;///记录该位权值
        a>>=1;///a右移,
    }
    return 0;///返回0代表全1
}

至此,这题就差不多结束了!
代码:

#include <iostream>
#include <cstring>
#define ll long long
using namespace std;
ll a[200011][2];//a[n][0]代表n点和谁相连接,a[n][1]代表这两点之间的权值
ll judge(ll a)
{
    ///返回a从右边数第一个0的权值
    ll res=1;
    while(a)///逐位遍历
    {
        if((a&1)==0)///当前最后一位是0
        {
            return res;///返回权值
        }
        ///当前最后一位不是0
        res<<=1;///记录该位权值
        a>>=1;///a右移,
    }
    return 0;///返回0代表全1
}
int main()
{
    ll n,t,sum;
    cin>>n;
    while(n--)
    {
        cin>>t;
        sum=0;
        memset(a,0,sizeof(a));
        for(ll j=2; j<=t; j++)
        {
            if(j%2==0)///偶数
            {
                a[j][0]=1;///让它连接1
                a[j][1]=0;///该边权值为0
            }
            else    ///奇数
            {
                if(!judge(j)) ///全1的情况,judge()返回的是0
                {
                    if(j+1>t)///j+1超范围,所以不能连接j+1
                    {
                        a[j][0]=1;///为了保证字典序最小,让它连接1
                        a[j][1]=1;///权值为0
                    }
                    else///j+1不超范围
                    {
                        a[j][0]=j+1;
                        a[j][1]=0;
                    }
                }
                else ///中间有0的情况
                {
                    a[j][0]=judge(j);
                    a[j][1]=0;
                }
            }
            sum+=a[j][1];
        }
        cout<<sum<<endl;
        for(ll i=2; i<t; i++)
            cout<<a[i][0]<<" ";
        cout<<a[t][0]<<endl;
    }
    return 0;
}

发布了67 篇原创文章 · 获赞 42 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_26235879/article/details/98336006
今日推荐