题目链接:
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;
}