2018“百度之星”程序设计大赛 - 复赛 1003 带劲的and和(位运算,很好的题)

本文探讨了一个基于图论的算法挑战,通过并查集和位运算技巧,解决了一个复杂的数学问题,即计算特定图中所有点对之间的带劲的and和。文章详细解释了如何使用并查集确定连通组件,并利用位运算优化计算过程。

Problem Description

度度熊专门研究过“动态传递闭包问题”,他有一万种让大家爆蛋的方法;但此刻,他只想出一道简简单单的题——至繁,归于至简。

度度熊有一张n个点m条边的无向图,第ii个点的点权为v_iv​i​​。

如果图上存在一条路径使得点ii可以走到点jj,则称i,ji,j是带劲的,记f(i,j)=1f(i,j)=1;否则f(i,j)=0f(i,j)=0。显然有f(i,j) = f(j,i)f(i,j)=f(j,i)。

度度熊想知道求出: 

其中\&&是C++中的and位运算符,如1&3=1, 2&3=2。

请将答案对10^9+710​9​​+7取模后输出。

Input

第一行一个数,表示数据组数TT。

每组数据第一行两个整数n,mn,m;第二行nn个数表示v_iv​i​​;接下来mm行,每行两个数u,vu,v,表示点uu和点vv之间有一条无向边。可能有重边或自环。

数据组数T=50,满足:

  • 1 \le n,m \le 1000001≤n,m≤100000
  • 1 \le v_i \le 10^91≤v​i​​≤10​9​​。

其中90%的数据满足n,m \le 1000n,m≤1000。

Output

每组数据输出一行,每行仅包含一个数,表示带劲的and和。

Sample Input

Copy

1
5 5
3 9 4 8 9 
2 1
1 3
2 1
1 2
5 2

Sample Output

99

题意:就是那个公式

思路:先判断两个点之间是不是连通,这是就想到了并查集,找出每个联通块,由给出的公式,模拟知,在一个连通块中,每一个数都要与连通块中其他的数,进行那个公式运算,刚开始,我暴力做的超时,当看到(vi&vj)时,你就应该想到位运算,还要有一个思想,就是不管是两个数相乘,还是求幂,都可以用位运算来解决

先把每个连通块中的数,从小到大排序,因为每个数都要与连通块中其他数进行 max(vi,vj) * (vi&vj)的运算,所以,排序,不影响,从小到大排序好处,先让前面小的进行位运算求和,再与当前这个大的相乘(因为当前这个与前面小的数相乘时,一定选的大 ,再乘于两个数相与,我们先让前面小数的进行位运算求和,在选出当前数二进制为1的位,进行相乘) 很好的做法 n*32的复杂度;

当一个集合中,任意一个数都 与集合中其他数,进行相乘一次仅一次且乘的是他们位运算相与的值(vi&vj)然后求和(如,每一个数,都与位置在它前面数进行相与,然后再相乘这个数,得出值求和)这种情况可以在 n*32的复杂度下求出,不用n*n/2的复杂度求。 

看到式子中有位运算,你就应该想到,用位运算求解;

代码:

#include<bits/stdc++.h>
using namespace std;
#define Max 100005
#define ll long long
#define mod 1000000007
 
int a[Max];
int n,m;
int f[Max];
int s[70];
vector<int>v[Max];  // 把每个联通块放在一起; 

int find(int x)
{
    if(f[x] == x)
        return x;
    else return f[x] = find(f[x]);
}
int book[Max];

int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
    
        for(int i = 1;i<=n;i++)
        {
            f[i] = i;
            scanf("%d",&a[i]);
            v[i].clear();
        }
        int x,y;
        for(int i = 1;i<=m;i++)
        {
            scanf("%d%d",&x,&y);
            int t1 = find(x);
            int t2 = find(y);
            if(t1!=t2)
            {
                if(t2>t1)
                    f[t2] = t1;
                else f[t1] = t2;
            }
        }
        memset(s,0,sizeof(s));
        memset(book,0,sizeof(book));
        for(int i = 1;i<=n;i++)
            v[find(i)].push_back(a[i]);
        ll ans = 0;
        for(int i  = 1;i<=n;i++)
        {
            int tt = find(i);
            if(!book[tt])
            {
                book[tt] = 1;
                if(v[tt].size()<=1)    continue;
                memset(s,0,sizeof(s));
                sort(v[tt].begin(),v[tt].end());
                for(int j = 0;j<v[tt].size();j++)
                {
                    int kk = v[tt][j];
                    int temp = kk;
                    int p = 0;
                    while(kk)            //变成位运算; 
                    {
                        if(kk&1) 
                        {
							// 这个取模很重要;让我改错改了老长时间; 
                            ans += ((ll)(temp)*(ll)s[p])%mod*(1LL<<p)%mod;   
                            ans%=mod;
                        }
                        s[p++] += kk&1;
                        kk>>=1;
                    }
                }
            }    
        }
        printf("%I64d\n",ans);
        
    }
    return 0;
}

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值