tyvj P2020 Rainbow 的信号 【位运算】

题目:

Freda发明了传呼机之后,rainbow进一步改进了传呼机发送信息所使用的信号。由于现在是数字、信息时代,rainbow发明的信号用N个自然数表示。为了避免两个人的对话被大坏蛋VariantF偷听T_T,rainbow把对话分成A、B、C三部分,分别用a、b、c三个密码加密。现在Freda接到了rainbow的信息,她的首要工作就是解密。Freda了解到,这三部分的密码计算方式如下:
在1~N这N个数中,等概率地选取两个数l、r,如果l>r,则交换l、r。把信号中的第l个数到第r个数取出来,构成一个数列P。
A部分对话的密码是数列P的xor和的数学期望值。xor和就是数列P中各个数异或之后得到的数; xor和的期望就是对于所有可能选取的l、r,所得到的数列的xor和的平均数。
B部分对话的密码是数列P的and和的期望,定义类似于xor和。
C部分对话的密码是数列P的or和的期望,定义类似于xor和。

 

分析:

(如果你想AC的话,请忽略此部分)

这个题的暴力其实很好打,直接进行无脑循环:(复杂度O(n^2)已经是优化过的了)

40分代码:

 

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
long long n;
long long a[100010];
long long Xor[5000][5000],And[5000][5000],Or[5000][5000];
double ans1=0,ans2=0,ans3=0; 
long long cnt=0;

long long read() {
    long long ret=0,flag=1;
    char ch=getchar();
    while(ch<'0' || ch>'9') {
        if(ch=='-')
            flag=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9') {
        ret=ret*10+(long long)(ch-'0');
        ch=getchar();
    }
    return ret*flag;
}


int main()
{
    n=read();
    cnt=n*n;
    for(int i=1;i<=n;i++)
        a[i]=read();
    memset(Xor,0,sizeof(Xor));
    memset(And,0,sizeof(And));
    memset(Or,0,sizeof(Or));
    for(int i=1;i<=n;i++)
    {
        Xor[i][i]=a[i];
        ans1+=Xor[i][i];
        And[i][i]=a[i];
        ans2+=And[i][i];
        Or[i][i]=a[i];
        ans3+=Or[i][i];
    }
    for(int i=n-1;i>=1;i--)
        for(int j=i+1;j<=n;j++)
            {
                int p=(i+j)/2;
                Xor[i][j]=Xor[i][p]^Xor[p+1][j];    
                ans1+=Xor[i][j]*2;
            }
    for(int i=n-1;i>=1;i--)
        for(int j=i+1;j<=n;j++)
            {
                int p=(i+j)/2;
                And[i][j]=And[i][p]&And[p+1][j];    
                ans2+=And[i][j]*2;
            }
    for(int i=n-1;i>=1;i--)
        for(int j=i+1;j<=n;j++)
            {
                int p=(i+j)/2;
                Or[i][j]=Or[i][p]|Or[p+1][j];    
                ans3+=Or[i][j]*2;
            }
    printf("%.3lf %.3lf %.3lf\n",ans1/cnt,ans2/cnt,ans3/cnt);
    return 0;
}
View Code

 

 

而正解其实也不远了,通过分析这三种运算符的特点,只关心1的贡献:

(^):

1^1=0;

0^1=1;

他的功能就是:只要遇到1,就把当前的异或和取反,因此只要遇到1,就交换之前1和0的个数(即所有的1都变成了0,所有的0都变成了1)并且给1的个数加1;(自己应当拿笔模拟一下)

 

(&):

1&1=1;

1&0=0;

他的功能就是:只要遇到0,后面的都是0,因此只要找到前一个0出现的位置作为断点,计算有多少1出现就行了;

 

(|):

1|0=1;

1|1=1;

他的功能就是:只要遇到1,后面的都是1,因此只要找到前一个1出现的位置作为断点,计算有多少1出现就行了;

 

下面是参考代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
int n;
int cnt[2],last_pos[2];
int a[100005],b[100005];
double ans_or=0,ans_xor=0,ans_and=0;

void cal(int x)
{
    fill(cnt,cnt+2,0);
    fill(last_pos,last_pos+2,0);
    for(int i=1;i<=n;i++) b[i]=((a[i]>>x)&1);
    for(int i=1;i<=n;i++)
    {
        if(i!=1)
        {
            ans_xor+=(double)(1<<x)/n/n*cnt[!b[i]];
            if (b[i]==0) ans_or+=(double)(1<<x)/n/n*last_pos[1];    
            else
            {
                ans_or+=(double)(1<<x)/n/n*(i-1);
                ans_and+=(double)(1<<x)/n/n*(i-1-last_pos[0]);
            }  
        }
        last_pos[b[i]]=i;
        if(b[i]==0) cnt[0]++;
        else
            swap(cnt[0],cnt[1]),cnt[1]++;
    }
    for(int i=1;i<=n;i++)
    if(b[i])
    {
        double tmp=(double)(1<<x)/n/n/2;
        ans_xor+=tmp;
        ans_or+=tmp;
        ans_and+=tmp;
    }
}

int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    for(int i=0;i<=30;i++) cal(i);
    printf("%.3lf %.3lf %.3lf",ans_xor*2,ans_and*2,ans_or*2);
    return 0;
}
View Code

 

posted @ 2017-07-21 16:44  Captain_fcj  阅读(220)  评论(0编辑  收藏  举报