简单路径(Tourist Attractions) 题解 (模拟+二进制压位储存)

来源:互联网 发布:淘宝价格趋势图 编辑:程序博客网 时间:2024/04/27 23:25

给定一张n个点的无向图,统计有多少条简单路径恰好经过了4个点。

背景

在美丽的比特镇一共有n个景区,编号依次为1到n,它们之间通过若干条双向道路连接。
Byteasar慕名来到了比特镇旅游,不过由于昂贵的门票费,他只能负担起4个景区的门票费。他可以在任意景区开始游览,然后结束在任意景区。
Byteasar的旅游习惯比较特殊,一旦他路过了一个景区,他就一定会进去参观,并且他永远不会参观同一个景区两次。所以他想知道,有多少种可行的旅游路线,使得他可以恰好参观4个景区呢?即,有多少条简单路径恰好经过了4个点。

Input

第一行包含两个整数n,表示景区的总数。
第2至第n+ 1行,每行一个长度为n的01字符串,第i+ 1行第j 个字符为0表示i 和j 之间没有道路,为1表示有一条道路。
输入数据保证(i;j)的连接情况等于(j; i)的连接情况,且(i;i)恒为0。

Output

输出一行一个整数,即可行的路线总数。

Example

tour.in tour.out 4 8 0101 1010 0101 1010

8条路线分别为:
1->2->3->4, 4->3->2->1,
2->3->4->1, 1->4->3->2,
3->4->1->2, 2->1->4->3,
4->1->2->3, 3->2->1->4。

Notes

测试点编号 n 1 = 5 2 = 10 3 = 20 4 = 50 5 = 300 6 = 300 7 = 300 8 = 1500 9 = 1500 10 = 1500

部分分做法

  1. 40分做法:暴力枚举,时间复杂度O(n^4)
  2. 70分做法:设经过的点为a-b-c-d,枚举中间边b-c,再枚举与b,c相连的点,设deg x 表示点x的度数,那么边b-c对答案的贡献为(deg b-1)(deg c- 1) - 经过b-c这条边的三元环个数。 计算三元环的个数只需要枚举除b,c之外的另一个点即可,时间复杂度O(n^3)

AC做法

70分算法的瓶颈在于三元环计数。
设S x 表示所有和x有边的点的集合,那么其实就是统计Sb与Sc的并集的元素数。将S用二进制压位存储即可并行计算。时间复杂度O(n^3/32)。

70分代码

统计Sb与Sc的并集的元素数未加优化,超时

#include<cstdio>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define LL long longusing namespace std;const int maxn=1505;int n,m,c,sum[maxn];int map[maxn][50];LL ans;char s[maxn];struct note{    int u,v;}a[maxn*maxn/2];void addedge(int x,int y){    a[++c].u=x;a[c].v=y;}int check(int x,int y)//统计并集元素数{    int tmp=0;    for (int i=1;i<=m;i++)    {        int t=map[x][i]&map[y][i];        while (t>0)        {            t&=t-1;            tmp++;        }    }    return tmp;}int main(){       scanf("%d",&n);    for (int i=1;i<=n;i++)    {        scanf("%s",s);        for (int j=1;j<i;j++)          if (s[j-1]-'0') addedge(i,j);//统计一条边即可,最后再*2        m=0;        int cnt=0;        LL num=0;        for (int j=1;j<=n;j++)        {                       if (s[j-1]-'0') sum[i]++;//sum记录与i点相连的点数            cnt++;            num=num*2+s[j-1]-'0';            if (cnt==31)//二进制压缩31位记一次(因为int有32位,最高位为符号位)            {                map[i][++m]=num;                num=cnt=0;            }        }        if (cnt!=0) map[i][++m]=num;    }    for (int i=1;i<=c;i++)    {        ans+=1LL*(sum[a[i].u]-1)*(sum[a[i].v]-1)-check(a[i].u,a[i].v);    }    ans=1LL*ans*2;    printf("%lld\n",ans);    return 0;}

计算二进制数中1的位数

这里介绍一个计算二进制数中1的位数的方法(原帖称作完美法?!!!)

以下从原帖中复制


int BitCount5(unsigned int n){    unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111);    return ((tmp + (tmp >>3)) &030707070707) %63;}

最喜欢这个,代码太简洁啦,只是有个取模运算,可能速度上慢一些。区区两行代码,就能计算出1的个数,到底有何奥妙呢?为了解释的清楚一点,我尽量多说几句。

第一行代码的作用

先说明一点,以0开头的是8进制数,以0x开头的是十六进制数,上面代码中使用了三个8进制数。

将n的二进制表示写出来,然后每3bit分成一组,求出每一组中1的个数,再表示成二进制的形式。比如n = 50,其二进制表示为110010,分组后是110和010,这两组中1的个数本别是2和3。2对应010,3对应011,所以第一行代码结束后,tmp = 010011,具体是怎么实现的呢?由于每组3bit,所以这3bit对应的十进制数都能表示为2^2 * a + 2^1 * b + c的形式,也就是4a + 2b + c的形式,这里a,b,c的值为0或1,如果为0表示对应的二进制位上是0,如果为1表示对应的二进制位上是1,所以a + b + c的值也就是4a + 2b + c的二进制数中1的个数了。举个例子,十进制数6(0110)= 4 * 1 + 2 * 1 + 0,这里a = 1, b = 1, c = 0, a + b + c = 2,所以6的二进制表示中有两个1。现在的问题是,如何得到a + b + c呢?注意位运算中,右移一位相当于除2,就利用这个性质!

4a + 2b + c 右移一位等于2a + b

4a + 2b + c 右移量位等于a

然后做减法

4a + 2b + c –(2a + b) – a = a + b + c,这就是第一行代码所作的事,明白了吧。

第二行代码的作用

在第一行的基础上,将tmp中相邻的两组中1的个数累加,由于累加到过程中有些组被重复加了一次,所以要舍弃这些多加的部分,这就是&030707070707的作用,又由于最终结果可能大于63,所以要取模。

需要注意的是,经过第一行代码后,从右侧起,每相邻的3bit只有四种可能,即000, 001, 010, 011,为啥呢?因为每3bit中1的个数最多为3。所以下面的加法中不存在进位的问题,因为3 + 3 = 6,不足8,不会产生进位。

tmp + (tmp >> 3)-这句就是是相邻组相加,注意会产生重复相加的部分,比如tmp = 659 = 001 010 010 011时,tmp >> 3 = 000 001 010 010,相加得

001 010 010 011
000 001 010 010
————————
001 011 100 101

011 + 101 = 3 + 5 = 8。(感谢网友Di哈指正。)注意,659只是个中间变量,这个结果不代表659这个数的二进制形式中有8个1。

注意我们想要的只是第二组和最后一组(绿色部分),而第一组和第三组(红色部分)属于重复相加的部分,要消除掉,这就是&030707070707所完成的任务(每隔三位删除三位),最后为什么还要%63呢?因为上面相当于每次计算相连的6bit中1的个数,最多是111111 = 77(八进制)= 63(十进制),所以最后要对63取模。


AC代码

#include<cstdio>#include<iostream>#include<algorithm>#include<cstring>#include<cmath>#define LL long longusing namespace std;const int maxn=1505;int n,m,c,sum[maxn];int map[maxn][50];LL ans;char s[maxn];struct note{    int u,v;}a[maxn*maxn/2];void addedge(int x,int y){    a[++c].u=x;a[c].v=y;}int check(int x,int y){    int tmp=0;    for (int i=1;i<=m;i++)    {        int t=map[x][i]&map[y][i];        unsigned int tt=t;        unsigned int st=tt - ((tt >>1) &033333333333) - ((tt >>2) &011111111111);        tmp+=((st + (st >>3)) &030707070707) %63;    }    return tmp;}int main(){    scanf("%d",&n);    for (int i=1;i<=n;i++)    {        scanf("%s",s);        for (int j=1;j<i;j++)          if (s[j-1]-'0') addedge(i,j);        m=0;        int cnt=0;        LL num=0;        for (int j=1;j<=n;j++)        {                       if (s[j-1]-'0') sum[i]++;            cnt++;            num=num*2+s[j-1]-'0';            if (cnt==31)            {                map[i][++m]=num;                num=cnt=0;            }        }        if (cnt!=0) map[i][++m]=num;    }    for (int i=1;i<=c;i++)    {        ans+=1LL*(sum[a[i].u]-1)*(sum[a[i].v]-1)-check(a[i].u,a[i].v);    }    ans=1LL*ans*2;    cout<<ans<<endl;//ans太大了lld输不了,建议用Int64d或cout    return 0;}

【小技巧】

从范围小的变量入手;点与边之间的转换

0 0