CQOI2018简要题解

CQOI2018简要题解

D1T1 破解 D-H 协议

题意

Diffie-Hellman 密钥交换协议是一种简单有效的密钥交换方法。它可让通信双方在没有事先约定密钥(密码)的状况下,经过不安全的信道(可能被窃听)创建一个安全的密钥 \(K\),用于加密以后的通信内容。ios

假定通信双方名为 Alice 和 Bob,协议的工做过程描述以下(其中 \(\bmod\) 表示取模运算):编程

  1. 协议规定一个固定的质数 \(P\),以及模 \(P\) 的一个原根 \(g\)\(P\)\(g\) 的数值都是公开的,无需保密。
  2. Alice 生成一个随机数 \(a\),并计算 \(A=g^a\bmod P\),将 \(A\) 经过不安全信道发送给 Bob。
  3. Bob 生成一个随机数 \(b\),并计算 \(B=g^b\bmod P\),将 \(B\) 经过不安全信道发送给 Alice。
  4. Bob 根据收到的 \(A\) 计算出 \(K=A^b\bmod P\),而 Alice 根据收到的 \(B\) 计算出 \(K=B^a\bmod P\)
  5. 双方获得了相同的 \(K\),即 \(g^{ab}\bmod P\)\(K\) 能够用于以后通信的加密密钥。

可见,这个过程当中可能被窃听的只有 \(A,B\),而 \(a,b,K\) 是保密的。而且根据 \(A,B,P,g\)\(4\) 个数,不能轻易计算出 \(K\),所以 \(K\) 能够做为一个安全的密钥。安全

固然安全是相对的,该协议的安全性取决于数值的大小,一般 \(a,b,P\) 都选取数百位以上的大整数以免被破解。然而若是 Alice 和 Bob 编程时偷懒,为了不实现大数运算,选择的数值都小于 \(2^{31}\),那么破解他们的密钥就比较容易了。网络

第一行包含两个空格分开的正整数 \(g\)\(P\)加密

第二行为一个正整数 \(n\),表示 Alice 和 Bob 共进行了 \(n\) 次链接(即运行了 \(n\) 次协议)。spa

接下来 \(n\) 行,每行包含两个空格分开的正整数 \(A\)\(B\),表示某次链接中,被窃听的 \(A,B\) 数值。code

题解

BSGS裸题。blog

代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
using namespace std;
const int Mo=100003;
int a,b,A,B,P,n;
struct HashTable
{
    struct Line{int next,v1,v2;}a[Mo];
    int head[Mo],cnt;
    void reset() {memset(head,0,sizeof(head));cnt=0;}
    void Add(int p,int v1,int v2) {a[++cnt]=(Line){head[p],v1,v2};head[p]=cnt;}
    int Query(int x)
        {
            for(int i=head[x%Mo];i;i=a[i].next)
                if(a[i].v1==x) return a[i].v2;return -1;
        }
}Hash;
int ksm(int x,int k)
{
    int s=1;for(;k;k>>=1,x=1ll*x*x%P)
                if(k&1) s=1ll*s*x%P;return s;
}
int BSGS(int A,int B,int P)
{
    int M=sqrt(P)+1;Hash.reset();
    for(int i=0,t=B;i<M;i++,t=1ll*t*A%P) Hash.Add(t%Mo,t,i);
    for(int i=1,bs=ksm(A,M),t=bs;i<=M;i++,t=1ll*t*bs%P)
        if(Hash.Query(t)!=-1) return i*M-Hash.Query(t);return -1;
}
int main()
{
    cin>>A>>P>>n;
    while(n--)
    {
        cin>>B>>b;a=BSGS(A,B,P);
        printf("%d\n",ksm(b,a));
    }
    return 0;
}

D1T2 社交网络

题意

当今社会,在社交网络上看朋友的消息已经成为许多人生活的一部分。一般,一个用户在社交网络上发布一条消息(例如微博、状态、Tweet 等)后,他的好友们也能够看见这条消息,并可能转发。转发的消息还能够继续被人转发,进而扩散到整个社交网络中。游戏

在一个实验性的小规模社交网络中咱们发现,有时一条热门消息最终会被全部人转发。为了研究这一现象发生的过程,咱们但愿计算一条消息全部可能的转发途径有多少种。为了编程方便,咱们将初始消息发送者编号为 \(1\),其余用户编号依次递增。ip

该社交网络上的全部好友关系是已知的,也就是说对于 A、B 两个用户,咱们知道 A 用户能够看到 B 用户发送的消息。注意可能存在单向的好友关系,即 A 能看到 B 的消息,但 B 不能看到 A 的消息。

还有一个假设是,若是某用户看到他的多个好友转发了同一条消息,他只会选择从其中一个转发,最多转发一次消息。从不一样好友的转发,被视为不一样的状况。

若是用箭头表示好友关系,下图展现了某个社交网络中消息转发的全部可能状况。(初始消息是用户 \(1\) 发送的,加粗箭头表示一次消息转发)

Ex1Ex2Ex3

Ex4Ex5Ex6

对于 \(30\%\) 的数据,\(1≤n≤10\)

对于 \(100\%\) 的数据,\(1\leq n\leq 250, 1\leq a_i,b_i\leq n, 1\leq m\leq n(n-1)\)

题解

矩阵树定理裸题。

对于有向图有根树的计数,边\(x->y\)则矩阵\(a[x][y]--,a[y][y]++\),即度数矩阵为入读。同时注意删去根所在的那一行一列。

代码

#include<iostream>
using namespace std;
const int P=10007;
int n,m,g[251][251],ans=1;
int main()
{
    cin>>n>>m;
    for(int i=1,a,b;i<=m;i++)
    {
        scanf("%d%d",&b,&a);
        g[a][b]--;g[b][b]++;
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            (g[i][j]+=P)%=P;
    for(int i=2;i<=n;i++)
        for(int j=i+1;j<=n;j++)
            while(g[j][i])
            {
                int t=g[i][i]/g[j][i];ans=-ans;
                for(int k=i;k<=n;k++)
                    g[i][k]=(P+g[i][k]-1ll*t*g[j][k]%P)%P,swap(g[i][k],g[j][k]);
            }
    for(int i=2;i<=n;i++) ans=1ll*ans*g[i][i]%P;
    cout<<(ans+P)%P<<endl;
}

D1T3 交错序列

题意

咱们称一个仅由 \(0,1\) 构成的序列为「交错序列」,当且仅当序列中没有相邻的 \(1\)(能够有相邻的 \(0\))。例如,\(000,001,101\) 都是交错序列,而 \(110\) 则不是。

对于一个长度为 \(n\) 的交错序列,统计其中 \(0\)\(1\) 出现的次数,分别记为 \(x\)\(y\)。给定参数 \(a,b\),定义一个交错序列的特征值为 \(x^ay^b\)。注意这里规定任何整数的 \(0\) 次幂都等于 \(1\)(包括 \(0^0=1\))。

显然长度为 \(n\) 的交错序列可能有多个。咱们想要知道,全部长度为 \(n\) 的交错序列的特征值的和,除以 \(m\) 的余数。(\(m\) 是一个给定的质数)

例如,所有长度为 \(3\) 的交错串为:\(000,001,010,100,101\)。当 \(a=1,b=2\) 时,可计算:\(3^1\times0^2+2^1\times1^2+2^1\times1^2+2^1\times1^2+1^1\times2^2=10\)

对于 \(30\%\) 的数据,\(1\leq n\leq 15\)

对于 \(100\%\) 的数据,\(1\leq n\leq 10^7,0\leq a,b\leq 45, m<10^8\)

题解

惟一一道有点意思的题目。

首先考虑\(\mathcal O(nlog)\)暴力:枚举\(1\)有多少个,此时0的方案数能够直接用组合数算:\(C(n+1-i,i)\)。有\(i\)个1,那么有\(n-i\)个0,咱们能够在这\(n-i\)个位置以及第0个位置、一共\(n+1-i\)个位置中,选择\(i\)个位置在后面放1。

因此答案是\(\sum_{i=0}^{\lfloor \frac{n+1}{2}\rfloor} C_{n+i-1}^{i}(n-i)^ai^b\)

然而这只有暴力的三十分,因为数据范围极具误导性,卡了半个小时常仍然无果(甚至试了线性筛快速幂)。

正解是这样的:用二项式定理拆开式子

\[x^ay^b=(n-y)^ay^b=\sum_{i=0}^aC_a^in^i(-y)^{a-i}y^b=\sum_{i=0}^a(-1)^{a-i}C_a^in^iy^{a+b-i}\]

如今考虑的就是\(y\)的全部方案的幂次和是什么,或者根据刚才的式子,能够获得

\[Ans=\sum_{y=0}^{\lfloor \frac{n+1}{2}\rfloor}C_{n+y-1}^y(n-y)^ay^b=\sum_{i=0}^{a}(-1)^{a-i}C_a^in^i\sum_{y=0}^{\lfloor \frac{n+1}{2}\rfloor}y^{a+b-i}C_{n+1-y}^y\]

如今咱们要求算\(f[k]\)表示后面那个\(\sum\)\(a+b-i=k\)时候的和。

\(f[k][i][0/1]\)表示长度为\(k\)的序列,后面那一坨指数是\(i\)的和。

有转移:

\[f[k][i][0]=f[k-1][i][0]+f[k-1][i][1]\]

表示在末尾接一个0,能够从1和0同时转移。

\[f[k][i][1]=\sum_{j=0}^i C_i^jf[k-1][i][0]\]

表示在末尾接一个1,至关于给\(y\)+1,二项式展开后是这幅面孔。

而后因为k比较大,因此经过矩阵转移,这种多维DP的矩阵会分区,好比这题的矩阵以下构造:

  • 答案矩阵,第\(1​\)~\(a+b+1​\)行第\(1\)列表示\(f[i][0]\);第\(a+b+2\)~\(2(a+b+1)\)\(1\)列表示\(f[i][1]\)
  • \(1\)~\(a+b+1\)行,\(1\)~\(a+b+1\)列,表示\(f[i][0]\)\(f[i][0]\)的转移,即\(G[i][i]=1\)
  • \(1\)~\(a+b+1\)行,\(a+b+2\)~\(2(a+b+1)\)列,表示\(f[i][1]\)\(f[i][0]\)的转移,即\(G[i][i]=1\)
  • \(a+b+2\)~\(2(a+b+1)\)行,\(1\)~\(a+b+1\)列,表示\(f[i][0]\)\(f[i][1]\)的转移,为杨辉三角。
  • \(a+b+2\)~\(2(a+b+1)\)行,\(a+b+2\)~\(2(a+b+1)\)列,表示\(f[i][1]\)\(f[i][1]\)的转移,全是\(0\)

代码

#include<iostream>
#include<cstring>
using namespace std;
const int N=1e7+10;
int n,a,b,p,C[100][100],l,Ans;
struct matrix
{
    long long a[200][200];
    matrix () {memset(a,0,sizeof(a));}
    matrix operator * (matrix A)
        {
            matrix C;
            for(int i=1;i<=l;i++)
                for(int j=1;j<=l;j++)
                {
                    for(int k=1;k<=l;k++)
                        C.a[i][j]+=a[i][k]*A.a[k][j];
                    C.a[i][j]%=p;
                }
            return C;
        }
}A,B;
int main()
{
    cin>>n>>a>>b>>p;
    for(int i=0;i<=99;i++) C[i][0]=1;
    for(int i=1;i<=99;i++)
        for(int j=1;j<=99;j++)
            C[i][j]=(C[i-1][j-1]+C[i-1][j])%p;
    l=(a+b+1)*2;
    for(int i=1;i<=l/2;i++)
    {
        A.a[i][i]=A.a[i][l/2+i]=1;
        for(int j=1;j<=i;j++)
            A.a[l/2+i][j]=C[i-1][j-1];
    }
    for(int i=1;i<=l;i++) B.a[i][i]=1;
    for(int k=n;k;k>>=1,A=A*A) if(k&1) B=B*A;
    for(int i=0,pw=1;i<=a;i++,pw=1ll*pw*n%p)
        (Ans+=1ll*(((a-i)&1)?-1:1)*C[a][i]*pw%p*(B.a[a+b-i+1][1]+B.a[l/2+a+b-i+1][1])%p)%=p;
    cout<<(Ans+p)%p;
}

D2T1 解锁屏幕

题意

使用过 Android 手机的同窗必定对手势解锁屏幕不陌生。 Android 的解锁屏幕由 \(3 \times 3\) 个点组成,手指在屏幕上画一条线,将其中一些点链接起来,便可构成一个解锁图案。以下面三个例子所示:

imgimgimg

画线时还须要遵循一些规则:

  1. 链接的点数不能少于 \(4\) 个。也就是说只链接两个点或者三个点会提示错误。
  2. 两个点之间的联线不能弯曲。
  3. 每一个点只能“使用”一次,不可重复。这里的“使用”是指手指划过一个点,该点变绿。
  4. 两个点之间的连线不能“跨过”另外一个点,除非那个点以前已经被“使用”过。

对于最后一条规则,参见下图的解释。左边两幅图违反了该规则;而右边两幅图(分别为 $ 2 \rightarrow 4 \rightarrow 1 \rightarrow 3 \rightarrow 6$ 和 $ 6 \rightarrow 5 \rightarrow 4 \rightarrow 1 \rightarrow 9 \rightarrow 2$ )则没有违反规则,由于在“跨过”点时,点已经被使用过了。

img

如今工程师但愿改进解锁屏幕,增减点的数目,并移动点的位置,再也不是一个九宫格形状,但保持上述画线规则不变。

请计算新的解锁屏幕上,一共有多少知足规则的画线方案。

对于 \(30\%\) 的数据, \(1 \le n \le 10\)
对于 \(100\%\) 的数据, \(-1000 \le x_i ,y_i \le 1000\) , $ 1 \le n < 20$ 。各点坐标不相同。

题解

状压DP。

代码

#include<iostream>
#include<cmath>
using namespace std;
const int mod=1e8+7;
int n,x[30],y[30],bit[30],S[30][30];
int dp[1<<20][21],ans;
double dis(int i,int j) {return sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));}
int main()
{
    cin>>n;bit[0]=1;
    for(int i=1;i<=n;i++) cin>>x[i]>>y[i];
    for(int i=1;i<=21;i++) bit[i]=1<<(i-1);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            for(int k=1;k<=n;k++)
                if(fabs(dis(i,j)-dis(i,k)-dis(j,k))<1e-8&&k!=i&&k!=j)
                    S[i][j]|=bit[k];
    for(int i=1;i<=n;i++) dp[bit[i]][i]=1;
    for(int zt=0;zt<bit[n+1];zt++)
        for(int i=1;i<=n;i++)
            if(dp[zt][i])
                for(int j=1;j<=n;j++)
                {
                    if(bit[j]&zt) continue;
                    if((S[i][j]&zt)!=S[i][j]) continue;
                    (dp[zt|bit[j]][j]+=dp[zt][i])%=mod;
                }
    for(int zt=0,res=0;zt<bit[n+1];res=0,zt++)
    {
        for(int i=1;i<=n;i++) if(zt&bit[i]) res++;
        if(res<=3) continue;
        for(int i=1;i<=n;i++) (ans+=dp[zt][i])%=mod;
    }
    cout<<ans<<endl;
}

D2T2 九连环

题意

九连环是一种源于中国的传统智力游戏。如图所示,九个圆环套在一把“剑”上,而且互相牵连。游戏的目标是把九个圆环从“剑”上卸下。

圆环的装卸须要遵照两个规则。

  1. 第一个(最右边)环任什么时候候均可以装上或卸下。
  2. 若是第 \(k\) 个环没有被卸下,且第 \(k\) 个环右边的全部环都被卸下,则第 \(k+1\) 个环(第 \(k\) 个环左边相邻的环)能够任意装上或卸下。

与魔方的变幻无穷不一样,解九连环的最优策略是惟一的。为简单起见,咱们以“四连环”为例,演示这一过程。这里用 1 表示环在“剑”上, 0 表示环已经卸下。

初始状态为 1111 ,每部的操做以下:

  1. 1101 (根据规则 \(2\) ,卸下第 \(2\) 个环)
  2. 1100 (根据规则 \(1\) ,卸下第 \(1\) 个环)
  3. 0100 (根据规则 \(2\) ,卸下第 \(4\) 个环)
  4. 0101 (根据规则 \(1\) ,装上第 \(1\) 个环)
  5. 0111 (根据规则 \(2\) ,装上第 \(2\) 个环)
  6. 0110 (根据规则 \(1\) ,卸下第 \(1\) 个环)
  7. 0010 (根据规则 \(2\) ,卸下第 \(3\) 个环)
  8. 0011 (根据规则 \(1\) ,装上第 \(1\) 个环)
  9. 0001 (根据规则 \(2\) ,卸下第 \(2\) 个环)
  10. 0000 (根据规则 \(1\) ,卸下第 \(1\) 个环)

因而可知,卸下“四连环”至少须要 \(10\) 步。随着环数增长,须要的步数也会随之增多。例如卸下九连环,就至少须要 \(341\) 步。
请你计算,有 \(n\) 个环的状况下,按照规则,所有卸下至少须要多少步。

对于 \(10\%\) 的数据, \(1 \le n \le 10\)
对于 \(30\%\) 的数据, \(1 \le n \le 30\)
对于 \(100\%\) 的数据, \(1 \le n \le 10^5 , 1 \le m \le 10\)

题解

经过这\(f[4]\)的演示,能够发现:\(f[4]=f[2]+1+f[2]+f[3]\),因此获得递推:\(f[i]=2f[i-2]+f[i]+1\)

因此经过观察可得,\(f[2k+1]=\frac{4^{k+1}-1}{3},f[2k]=\frac{f[2k+1]-1}{2}\)

因而把\(4\)的1到16次方高精度预处理出来,而后直接上高精度乘法除法就行了。

不知道为何要FFT

代码

#include<iostream>
#include<cstring>
using namespace std;
int m,n;
struct Bignum
{
    int a[100000],l;
    Bignum () {memset(a,0,sizeof(a));l=0;}
    void del()
        {
            a[1]--;
            for(int i=1;i<l;i++)
                if(a[i]<0) a[i]=9,a[i+1]--;
        }
    void div(int x)
        {
            for(int i=l,res=0;i>=1;i--)
                res+=a[i],a[i]=res/x,res%=x,res*=10;
            while(!a[l]) l--;
        }
    Bignum operator * (Bignum A)
        {
            Bignum C;C.l=A.l+l-1;
            for(int i=1;i<=l;i++)
                for(int j=1;j<=A.l;j++)
                    C.a[i+j-1]+=a[i]*A.a[j];
            for(int i=1;i<C.l;i++) C.a[i+1]+=C.a[i]/10,C.a[i]%=10;
            while(C.a[C.l]>9) C.a[C.l+1]+=C.a[C.l]/10,C.a[C.l]%=10,C.l++;
            while(!C.a[C.l]) C.l--;
            return C;
        }
    void print() {for(int i=l;i>=1;i--) printf("%d",a[i]);puts("");}
}bs[17],Ans;
int main()
{
    bs[0].l=1;bs[0].a[1]=4;
    for(int i=1;i<=16;i++) bs[i]=bs[i-1]*bs[i-1];
    for(cin>>m;m;m--)
    {
        cin>>n;Ans.l=Ans.a[1]=1;
        int k=(n-(n&1))/2+1;
        for(int i=0;i<=16;i++)
            if(k&(1<<i)) Ans=Ans*bs[i];
        Ans.del(),Ans.div(3);
        if(!(n&1)) Ans.del(),Ans.div(2);
        Ans.print();
    }
    return 0;
}

D2T3 异或序列

题意

已知一个长度为 \(n\) 的整数数列 \(a_1,a_2,\dots,a_n\) ,给定查询参数 \(l\)\(r\) ,问在 \(a_l,a_{l+1},\dots,a_r\) 区间内,有多少子序列知足异或和等于 \(k\) 。也就是说,对于全部的 $ x,y ( l \le x \le y \le r)$ ,知足 $ a_x \oplus a_{x+1} \oplus \dots \oplus a_y=k $ 的 \(x,y\) 有多少组。

对于 \(30\%\) 的数据, \(1 \le n,m \le 1000\)
对于 \(100\%\) 的数据, \(1 \le n,m \le 10^5 , 0 \le k,a_i \le 10^5 , 1 \le l_j \le r_j \le n\)

题解

处理好异或前缀和后,也就是区间\(l,r\)内每一对\(s[i]=x,s[j]=x\oplus k\)都会对答案产生1的贡献。

用桶而后莫队就行了。

代码

#include<iostream>
#include<algorithm>
#include<cmath>
#define ll long long
using namespace std;
const int N=5e5;
int n,m,k,a[N],s[N],bel[N],tong[N],blk;
ll Ans;
struct Que{int l,r,id;ll ans;}Q[N];
int cmp (const Que&A,const Que&B) {return bel[A.l]==bel[B.l]?A.r<B.r:bel[A.l]<bel[B.l];}
int cmp1(const Que&A,const Que&B) {return A.id<B.id;}
void Add(int p) {Ans+=tong[s[p]^k];tong[s[p]]++;}
void Del(int p) {Ans-=tong[s[p]^k];tong[s[p]]--;}
int main()
{
    cin>>n>>m>>k;
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),s[i]=s[i-1]^a[i];
    for(int i=1;i<=m;i++) scanf("%d%d",&Q[i].l,&Q[i].r),Q[i].id=i,Q[i].l--;
    blk=sqrt(n);
    for(int i=0,nw=0;i<=n;bel[i]=nw,i++) if(i%blk==0) nw++;
    sort(Q+1,Q+m+1,cmp);
    for(int i=1,l=1,r=0;i<=m;i++)
    {
        while(r<Q[i].r) Add(++r);
        while(l>Q[i].l) Add(--l);
        while(r>Q[i].r) Del(r--);
        while(l<Q[i].l) Del(l++);
        Q[i].ans=Ans;
    }
    sort(Q+1,Q+m+1,cmp1);
    for(int i=1;i<=m;i++) printf("%lld\n",Q[i].ans);
    return 0;
}

后记

CQOI搞什么啊题目质量一点都不行啊。

5道模板真的使人无发可说。。

自测起来除了D1T3大概是50分外,其余都秒了。

可是考场不开O2的话高精度可能会被卡。

总之最多550也是十分危险的,由于若是Noip是发挥良好的话是520的队线,可是我Noip炸了啊!

愿HNOI不会这样。。

相关文章
相关标签/搜索