2020.07.22 福州大学第十七届程序设计竞赛-题解 (FZU/FOJ 2317-2326)

本人联系方式

  1. 根号 \(2\) (FZU/FOJ 2317)

    \(a_0=\sqrt 2,a_{n+1}=[a_n]+{1\over \{a_n\}}\),这里 \([x]\)\(\{x\}\) 分别表示 \(x\) 的整数与小数部分,现给定一个数 \(k\) ,求 \(a_k-a_0\) 的值。

传送门

知识点:数学

考虑到 \([\sqrt 2]=1,\{\sqrt 2\}=\sqrt 2-1\)

\(\therefore a_1=[a_0]+{1\over \{a_0\}}=1+{1\over \sqrt 2-1}=1+{\sqrt 2+1\over 2-1}=\sqrt 2+2\)

\(\therefore a_2=[a_1]+{1\over \{a_1\}}=3+{1\over \sqrt 2-1}=3+{\sqrt 2+1\over 2-1}=\sqrt 2+4\)

可以用数学归纳法证明一下, \(a_n=\sqrt 2+2n\)\(a_k-a_0=2k\)

数据比较大,开 long long 可过

【代码】

#include<iostream>
using namespace std;
typedef long long ll;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    ll K;
    while(cin>>K){
        cout<<2*K<<endl;
    }
    return 0;
}

  1. 矩阵 (FZU/FOJ 2318)

    给定两个 \(n\) 阶方阵 \(A\)\(B\) ,矩阵 \(C=A\times B\) ,现在有个 \(q\) 询问,每次询问修改矩阵 \(A\) 或矩阵 \(B\) 的一个元素,请求出每次修改后矩阵 \(C\) 的主对角线元素之和.

传送门

知识点:数学优化预处理

先读入矩阵 \(A\)\(B\) ,然后暴力算出 \(C\) 主对角线上的元素(别的不要算,不然时间复杂度也不够)

for(int i=1;i<=N;i++)
      for(int j=1;j<=N;j++)
            C[i]+=A[i][j]*B[j][i];

接着只需要 \(O(n)\) 求和 \(C\) 的主对角线元素即可得到初始和

接下来考虑修改:若修改位置为 \(A_{ij}\) ,则会影响到的 \(C\) 中的主对角线元素仅有 \(C_{ii}\) ,而 \(C_{ii}\) 中需要修改的也仅有 \(A_{ij}B_{ji}\) 项的积

因此修改只需要 \(O(1)\)

SumC-=C[i];
C[i]-=A[i][j]*B[j][i];
A[i][j]=v;
C[i]+=A[i][j]*B[j][i];
SumC+=C[i];

同理考虑修改项为 \(B_{ij}\) 即可(注意此时修改到的为 \(C_{jj}\) 项)

【代码】

#include<iostream>
using namespace std;
typedef long long ll;
const ll MAXN=1024;
ll N,Q,A[MAXN][MAXN],B[MAXN][MAXN],C[MAXN],SumC;
inline ll ans(){
    ll K,X,Y,V;
    cin>>K>>X>>Y>>V;
    ll &c=((K==0)?C[X]:C[Y]),&p=((K==0)?A[X][Y]:B[X][Y]),&q=((K==0)?B[Y][X]:A[Y][X]);
    //用个引用,写起来会更舒适
    SumC-=c;
    c-=p*q;
    p=V;
    c+=p*q;
    return SumC+=c;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    while(cin>>N>>Q){
        for(int i=1;i<=N;i++)
            for(int j=1;j<=N;j++)
                cin>>A[i][j];
        for(int i=1;i<=N;i++)
            for(int j=1;j<=N;j++)
                cin>>B[i][j];
        SumC=0;
        for(int i=1;i<=N;i++) C[i]=0;
        for(int i=1;i<=N;i++)
            for(int j=1;j<=N;j++)
                C[i]+=A[i][j]*B[j][i];
        for(int i=1;i<=N;i++) SumC+=C[i];

        while(Q--) cout<<ans()<<endl;
    }
    return 0;
}

  1. 棋盘 (FZU/FOJ 2319)

    \(n\times m\) 的棋盘中,挖掉两个格子,使得可以用 \(1\times 2\) 或者 \(2\times 1\) 的骨牌不重叠不遗漏覆盖满除两个被挖掉格子之外的其他格子。
    输出挖掉两个格子的方案数。

传送门

知识点:数学特况

显然,若 \(nm\) 是奇数的时候,挖掉两个格子还剩下奇数个,由于每次都覆盖两个格子,所以一定无解

否则我们考虑左上角的格子是黑色,往右和往下白黑白黑依次排列

显然,每次的覆盖一定是覆盖一个黑一个白,故我们不能选择两个同样是黑或同样是白的格子,否则会造成一种颜色的格子少于另一个,无法覆盖

而黑格子的选法有 \(nm\over 2\) 种,白格子有 \(nm\over 2\) 种,故总方案数为 \(({nm\over 2})^2\) 种(可以证明,这种方法一定有解,除了下面那个情况)

这个解法获得 \(90pts\) ,还有一个点在于较短边为 \(1\) 的时候,此时不妨想象为一行的情况(否则逆时针旋转 \({\pi\over 2}\)),最左边一定是黑色

若我们选择的两个方块,处于更左边的那个是白色,则第一个方块左边将产生奇数个方块,无法填满,故左边那个是白色的应该删除

因此,对于双奇数的特判 \(0\) ,一边为 \(1\) 的特殊化处理:左边那个为黑色,剩下的输出 \(({nm\over 2})^2\) 即可过

下面证明,最短边大于 \(1\) 时,选择一黑一白一定能被覆盖:

首先,将选择的一黑一白看成某一矩形的两对角。十分显然,一个长宽乘积为矩形一定可被覆盖。

当矩形宽为奇数时,不妨设矩形宽垂直于底边。由于矩形宽为奇数,故矩形长一定为偶数。而矩形两对角分别竖直填充骨牌,因为宽为奇数,一定能刚好覆盖两个宽所在列。此时构成一个宽不变,长减少二的矩形。由于长度还是偶数,故可被覆盖。

而剩下部分显然可划分成若干长宽乘积为偶数的矩形,这里就不再赘述了。因此一定是可覆盖的。

同理考虑矩形宽为偶数,此时长必定为奇数的情况即可证明。

【代码】

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
inline ll ans(ll N,ll M){
    if((N&1)&&(M&1)) return 0;
    if( min(N,M)>1 ) return (N*M/2)*(N*M/2);
    if(N>M) swap(N,M);
    ll Ans=0;
    for(int i=0;i<M;i+=2) Ans+=(M-i>>1);
    return Ans;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    ll N,M;
    while(cin>>N>>M) cout<<ans(N,M)<<endl;
    return 0;
}

  1. 切割逆序对 (FZU/FOJ 2320)

    给定长度为 \(n\) 的序列 \(a[1..n]\),你可以选择一个 \(k\in [1,n−1]\) ,切 \(k\) 刀将 \(a\) 分成 \(k+1\) 非空段。
    求所有段内,逆序对个数之和的最大值,注意:单个元素逆序对个数为 \(0\)
    注:若 \(1\leq i<j\leq n\)\(a[i]>a[j]\),则称 \((i,j)\) 是一个逆序对

传送门

知识点:贪心树状数组/线段树

考虑到两个被分隔开的序列 \(AB\) ,若它们合并为一个序列,则逆序对数一定增加。因为 \(A,B\) 内部的逆序对数不会减少,还会增加形如 \(a>b,a\in A,b\in B\) 的逆序对

故两个序列合并后,对逆序对数的贡献一定更优,因此分割应尽可能少,只考虑分 \(1\)

对于每个元素,我们如果得知以这个元素为右端点的前缀的逆序对数,和以下一个元素为左端点的后缀逆序对数,则可 \(O(n)\) 扫过去,求出最大值

现考虑如何求出前缀逆序对数:当扫描到数 \(a_i\) 时,将其之前的所有元素扔进树状数组或线段树内,查询前面所有数中比它大的数的数量,即为 \(query(+\infty)-query(a_i)\) 。这样即可求出当前数对逆序对数的贡献,做前缀和即可得到前缀的逆序对数。

后缀的逆序对数也可类似的求解,故总复杂度为 \(O(n\log n)\)

【代码】

#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
const ll MAXN=1e5+10;
ll N,A[MAXN],L[MAXN],R[MAXN],BIT[MAXN];
inline ll lowbit(ll n) { return n&(-n); }//树状数组常规 lowbit 操作
inline ll query(ll pos){//树状数组查询
    ll ans=0;
    for(;pos;pos-=lowbit(pos)) ans+=BIT[pos];
    return ans;
}
inline void add(ll pos){//树状数组增加
    for(;pos<=1e5+5;pos+=lowbit(pos)) BIT[pos]++;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    while(cin>>N){
        for(int i=1;i<=N;i++) cin>>A[i];
        memset(BIT,0,sizeof(BIT));
        L[0]=R[N+1]=0;
        for(int i=1;i<=N;i++) L[i]=L[i-1]+query(1e5+5)-query(A[i]),add(A[i]);
        memset(BIT,0,sizeof(BIT));
        for(int i=N;i>=1;i--) R[i]=R[i+1]+query(A[i]-1),add(A[i]);
        ll Ans=0;
        for(int i=1;i<N;i++) Ans=max(Ans,L[i]+R[i+1]);
        cout<<Ans<<endl;
    }
    return 0;
}

  1. 竞赛图 (FZU/FOJ 2321)

    我们定义竞赛图为一张有向图,且 \(\forall i,j(i\neq j)\) ,点 \(i\) 与点 \(j\) 之间存在且仅存在一条边,即边 \(i\to j\) 或边 \(j\to i\) 。若 \(\exist i,j,k\) ,使得边 \(i\to j,j\to k,k\to i\) 均存在,我们称 \(i,j,k\) 构成一个三元环。
    但是很不幸的是,ZYZ 学长每次遇到 \(3\) 这个数字,总没好运,或 WA,或 TLE,或 RE,因此,现在 ZYZ 学长要进行以下操作,使得图不存在三元环:
    每次选取一条边,我们假设这条边是 \(i\to j\) , ZYZ 学长会将这条边删除,同时,因为 ZYZ 学长喜欢竞赛,他会让原图仍然是一张竞赛图,所以会同时新增一条 \(j\to i\) 的边。
    因为日理万机,ZYZ 学长想要花费尽量少的时间进行操作,请你帮他算出需要进行的最少操作数。

传送门

知识点:排列有向完全图的性质

题意为,给定一个 \(n\) 元有向完全图,每次操作可以翻转一条边。求最少的操作次数,使得图中不存在三元环。

考虑到对于 \(n\) 元有向完全图,若存在 \(n\) 元环,则一定存在三元环。证明:

因为有向完全图中存在 \(n\) 元环,故一定存在某个排列 \(p_{1\cdots n}\) 使得 \(p_1\to p_2\to p_3\to \cdots p_n\to p_1\) 。考虑点 \(p_{n-1}\)\(p_1\) 之间的边,若 \(p_1\to p_{n-1}\) 则存在三元环 \(p_1\to p_{n-1}\to p_n\to p_1\) ,否则存在 \((n-1)\) 元环 \(p_1\to p_2\to p_3\to \cdots p_{n-1}\to p_1\) 。递归下去,一定存在三元环。

因为有向完全图中不存在二元环和自环,且要满足答案需求(不存在三元环),则不能存在 \(n\) 元环。故完全图中不存在环。因此,答案的完全图为一个 DAG

设该 DAG 满足 \(p_1\to p_2\to p_3\to\cdots p_n\) 则一定满足 \(p_i\to p_j(i<j)\) ,因此我们只需要枚举排列即可:

枚举 \(n!\) 个排列,即可知道在满足答案为该排列条件下,各条边的指向。如果存在反边的,则用一个花费调换。求最小开销即可。

枚举边需要 \(O(n^2)\) 的时间,故总开销为 \(O(n!\cdot n^2)\)

【代码】

#include<iostream>
#include<algorithm>
using namespace std;
int N,A[16],C[16][16];
inline int calc(){
    int Ans=0;
    for(int i=1;i<=N;i++)
        for(int j=i+1;j<=N;j++)
            Ans+=C[ A[j] ][ A[i] ];
    return Ans;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    while(cin>>N){
        for(int i=1;i<=N;i++) A[i]=i;
        int Ans=0x3f3f3f3f;

        for(int i=1;i<=N;i++)
            for(int j=1;j<=N;j++){
                char c=0;
                while(c!='0'&&c!='1') cin>>c;
                C[i][j]=(c&1);
            }
        do{
            Ans=min(Ans,calc());
        }while( next_permutation(A+1,A+1+N) );
        cout<<Ans<<endl;
    }
    return 0;
}

  1. 开普勒表示 (FZU/FOJ 2322)

    我们可以从一个排列计算其开普勒表示,由排列 \(a_{​1,2,..,n}\)​​ 计算其开普勒表示 \(K_{​1,2,...,n}\)​​ ,方法如下:
    建立 \(n\) 个带编号的点,编号为 \(1,2,...,n\) ,初始没有边。
    连有向边 $i\to $${a_​i}​ ,i\in [1,n]$
    由于 \(a\) 是一个排列,那么这个有向图必然由若干个有向圈构成,注意这里圈长可能为 \(1\) ,即可能出现自环。
    对每个圈,从任意一个点出发,沿着边方向,遍历一遍(不重复访问点),按照顺序写下访问到的点 编号,并加括号,如: \((2,5,3,4,9)\) 表示一个圈 \(2\to 5\to 3\to 4\to 9\to 2\),当然表示不唯一,例如 \((3,4,9,2,5)\)
    将所有圈的表示放在一起,此时圈与圈的相对顺序可以任意,例如:\((7,8,10,9)(1,5,3)(4,2)(6)\)
    将每个圈旋转到以圈内最大值开头,例如:\((10,9,7,8)(5,3,1)(4,2)(6)\)
    确定圈与圈之间的顺序,以圈内最大值为关键字,从小到大排列:\((4,2)(5,3,1)(6)(10,9,7,8)\) 去掉括号,得到开普勒表示:\(4\) \(2\) \(5\) \(3\) \(1\) \(6\) \(10\) \(9\) \(7\) \(8\)
    当读入指令 \(op=1\) 时将 \(a\) 翻译为 \(K\) ,读入指令 \(op=2\) 时将 \(K\) 反翻译为 \(a\)

传送门

知识点:模拟图的遍历贪心

\(a\) 翻译为 \(K\) 的步骤根据题意即可,从未访问到的点开始 bfs 一下,每个点 \(i\) 的后继节点为 \(a_i\) ,直到后继节点已经访问过。此时的这些点构成一个环,将这些点扔进队列,并标记最大值。然后不停重复执行将队首扔进队尾,直到最大值为队首。最后把所有这样处理出来的队列,按首元素从大到小排序,再输出即可

\(K\) 反翻译为 \(a\) 需要注意到,\(K\) 中每个元素 \(K_i\) 后面的第一个比它大的数 \(K_j\) 一定不与它处于一个环内,且 \(K_j\) 前的一定处于一个环内。故对一个环内的依次建边 \((i\to a_i)\) ,再输出即可

【代码】

#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
const int MAXN=2e6+10;
int N,A[MAXN],K[MAXN],Vis[MAXN];
queue<int> q;
vector< queue<int> > v;
inline bool cmp(const queue<int> &a,const queue<int> &b) { return a.front()<b.front(); }
inline void search(int pos){
    int MaxNum=pos;
    while(!Vis[pos]){//遍历图
        MaxNum=max(MaxNum,pos);
        Vis[pos]=1;
        q.push(pos);
        pos=A[pos];
    }
    while(q.front()!=MaxNum) q.push( q.front() ),q.pop();
    //将最大的元素放至队首
    v.push_back(q);
}
inline void work1(){
    cin>>N;
    for(int i=1;i<=N;i++) cin>>A[i],Vis[i]=0;
    v.clear();
    for(int i=1;i<=N;i++) if(!Vis[i]){
        while( q.size() ) q.pop();
        search(i);
    }
    sort(v.begin(),v.end(),cmp);
    for(int i=0,I=v.size();i<I;i++){
        queue<int> &p=v[i];
        while(p.size()>1) cout<<p.front()<<" ",p.pop();
        cout<<p.front(); p.pop();
        if(i+1!=I) cout<<" ";
    }
    cout<<endl;
}
inline void work2(){
    cin>>N;
    for(int i=1;i<=N;i++) cin>>K[i];
    K[N+1]=0x3f3f3f3f;
    for(int l=1,r;l<=N;l=r+1){
        r=l;
        while(K[r+1]<K[l]) r++;
        for(int i=l;i<r;i++) A[ K[i] ]=K[i+1];//从当前元素指向下一元素
        A[ K[r] ]=K[l];
    }
    for(int i=1;i<N;i++) cout<<A[i]<<" ";
    cout<<A[N]<<endl;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int op;
    while(cin>>op)
        if(op==1) work1();
        else work2();
    return 0;
}

  1. 字符串 (FZU/FOJ 2323)

    给定空字符串 \(S\) ,执行以下 \(Q\) 次操作:
    每次操作形式:
    \(0\) \(c\) 表示在 \(S\) 之前加字符 \(c\)
    \(1\) \(c\) 表示在 \(S\) 之后加字符 \(c\)
    \(2\) \(t\) 询问 \(t\) 是否是 \(S\) 的一个带余周期,(设当前 \(S\) 长度为 \(len\) ,对任意 \(0\leq i\leq len-t-1\) , 都满足 \(S_i=S_{i+t}\)
    \(3\) 询问 \(S\) 是否是回文串;
    强制在线

传送门

知识点:字符串哈希树状数组/线段树卡常预处理

由于操作 \(0,1,3\) 比较容易理解,这里考虑一下操作 \(2\)

由于满足 \(S_i=S_{i+t}\)\(S_i=S_{i+kt}\) ,所以形如 abcdab 也满足带余周期为 \(4\) 。故考虑如何确定是否满足 \(t\) 为其带余周期:

很显然,若满足 \(t\) 为其带余周期,则 \(S_{len-1}=S_{len-t-1},S_{len-2}=S_{len-t-2},\cdots ,S_t=S_0\) 。因此我们暴力判断是否 \(S_{0\cdots len-t-1}=S_{t\cdots len-1}\) ,也就是判定其前 \((len-t)\) 的子串和后 \((len-t)\) 的子串是否相等。

本人因为个人编程习惯从 \(1\) 开始计数,下面可能会描述为 \(S_{1\cdots len-t}\)\(S_{t+1\cdots len}\)

由于 \(t\) 的长度最小为 \(0\) ,比对复杂度可能较高,考虑使用哈希实现:直接比较前 \((len-t)\) 的子串和后 \((len-t)\) 的子串是否哈希值相同。

而考虑回文的话,我们可以一开始就建好一个相反的串,然后比较两串的哈希值是否相同。

现在的问题变成了:如何实现可以从前面插入字符、后面插入字符以及查询字符串的哈希值。

我们假设存在一个足够大长度,初始值均为 \(0\) 的字符串,即哈希值初始为 \(0\) 。介于可能前插,也可能后插,故初始时,左指针和右指针都指向中间的相邻位置。而前插操作就是将值插入左指针的位置,而后左指针左移,后插类似。

而假设将字符 \(c\) 插入从左往右第 \(pos\) 位,那么对哈希值的贡献就是 \((c\cdot bas^{pos})\bmod mod\) 。因此我们预处理哈希值基数 \(bas\) 的次方,向位置 \(pos\) 插入字符 \(c\) 即为修改该位的哈希值为 \((c\cdot bas^{pos})\bmod mod\)

那么,查询操作也就十分显然了,求 \([l,r]\) 的子串哈希值,即为该部分哈希值之和,乘上 \(bas^{-l}\bmod mod\) 消除影响。

这是,我们需要快速的单点修改与区间查询,可以使用线段树或者树状数组实现。

此时,思路极为明确了:建立两个树状数组(或线段树),每个都开左指针与右指针,存放当前位置。将字符 \(c\) 插入位置 \(pos\) 即数据结构的位置 \(pos\) 的值增加 \((c\cdot bas^{pos})\bmod mod\) 。四个操作对应实现如下:
操作 \(0\) : 插入第一个的左指针和第二个的右指针;
操作 \(1\) : 插入第一个的右指针和第二个的左指针;
操作 \(2\) : 查询第一个 \([l+1,l+len-t]\) 的哈希值与 \([l+t+1,l+len]\) 的哈希值是否相同;
操作 \(3\) : 查询第一个 \([l,r]\) 的哈希值与第二个 \([l,r]\) 的哈希值是否相同。

至于强制在线的处理,可以理解为 \(F\) 初始为 \(11_{(2)}=3\) ,然后每次的答案 \(res\) 对它的影响为 F=(F<<1|res)&3

当时我这么想完,按面向对象封装好,再搞了个双哈希,非常优美的代码,被出题人卡 TLE 了。

事实证明,咱们搞 ACM 的时候还是不要太在意面向对象 所以需要卡一卡常数:首先,双哈希就免了,除非用小模数(但 \(10^6\) 的数据用小数没很大意义)。而模数可以直接选择 \(2^{64}\) ,这样刚好达到 unsigned long long ,可以自然溢出,速度极快。封装的全部拆了,不然会降低运行速率。差不多就这样了。

这边额外补充一下逆元的那一行代码 Inv[int(2e6)]=fpow(Pow[int(2e6)],(~0ull)>>1);
根据欧拉定理 \(a^{\varphi(m)}\equiv 1\pmod m,gcd(a,m)=1\) 因此我们选择质数 \(p>2\)\(k\) 次方 \(p^k\) 的话,则 \((p^k)^{-1}\cdot p^k\equiv 1\equiv (p^k)^{\varphi(2^{64})}\pmod {2^{64}}\) 从而得到 \((p^k)^{-1}\equiv (p^k)^{\varphi(2^{64})-1}\pmod {2^{64}}\)
介于 \(\varphi(2^{64})=2^{63}\varphi(2)=2^{63}\)\(\varphi(2^{64})-1=2^{63}-1\) 。在二进制意义下,其相当于第一位为 \(0\) ,后 \(63\) 位为 \(1\) 的 unsigned long long
使用 ~0ull 可快速表示 \(64\) 位均为 \(1\) 的二进制数,右移一位即得到所需数字

【代码】

#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const ull MAXN=2e6+10;

ull Q,BIT[2][MAXN],L[2],R[2],Len,Pow[MAXN],Inv[MAXN];

inline ull fpow(ull a,ull x){//快速幂
    ull ans=1;
    for(;x;x>>=1,a=a*a) if(x&1) ans=ans*a;
    return ans;
}
inline ll lowbit(ll n) { return n&(-n); }//树状数组常规 lowbit 操作
inline void add(ull bit[MAXN],ull pos,ull val){//树状数组加法
    for(;pos<=2e6;pos+=lowbit(pos)) bit[pos]+=val;
}
inline ull query(ull bit[MAXN],ll pos){//树状数组查询
    ull ans=0;
    for(;pos;pos-=lowbit(pos)) ans+=bit[pos];
    return ans;
}

inline void clear() { memset(BIT,0,sizeof(BIT)); L[0]=L[1]=1e6+3; R[0]=R[1]=1e6+4; Len=0; }//清空数据
inline void insertFront(ull bit[MAXN],ull &l,ull &r,ull c){//从前端插入字符(和树状数组是反着的)
    add(bit,r,c*Pow[r]);
    r++;
}
inline void insertBack(ull bit[MAXN],ull &l,ull &r,ull c){//从后端插入字符
    add(bit,l,c*Pow[l]);
    l--;
}
inline ull query(ull bit[MAXN],ull &l,ull &r,ull head,ull tail){//查询子串哈希值
    return ( query(bit,l+tail)-query(bit,l+head-1) )*Inv[l+head];
}
inline void work(){
    ull F=3,P,T;
    char c;
    while(Q--&&cin>>P){
        P^=F;
        if(P==0){
            cin>>c;
            insertFront(BIT[0],L[0],R[0],c);//正序哈希值从前插入
            insertBack(BIT[1],L[1],R[1],c);//逆序哈希值从后插入
            Len++;//长度增加
        }
        else if(P==1){
            cin>>c;
            insertBack(BIT[0],L[0],R[0],c);
            insertFront(BIT[1],L[1],R[1],c);
            Len++;
        }
        else if(P==2){
            cin>>T;
            if( query(BIT[0],L[0],R[0],1,Len-T)==query(BIT[0],L[0],R[0],T+1,Len) ){
                cout<<"Yes"<<endl;
                F=(F<<1|1)&3;
            }
            else{
                cout<<"No"<<endl;
                F=(F<<1)&3;
            }
        }
        else if(P==3){
            if( query(BIT[0],L[0],R[0],1,Len)==query(BIT[1],L[1],R[1],1,Len) ){
                cout<<"Yes"<<endl;
                F=(F<<1|1)&3;
            }
            else{
                cout<<"No"<<endl;
                F=(F<<1)&3;
            }
        }
    }
}

inline void pre(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    Pow[0]=1;
    Pow[1]=821;
    for(int i=2;i<=2e6;i++) Pow[i]=Pow[i-1]*Pow[1];
    Inv[int(2e6)]=fpow(Pow[int(2e6)],(~0ull)>>1);
    for(int i=2e6;i>=1;i--) Inv[i-1]=Inv[i]*Pow[1];
}
int main(){
    pre();
    while(cin>>Q){
        clear();
        work();
    }
    return 0;
}

  1. 二人游戏 (FZU/FOJ 2324)

    SY 和 JJ 两个玩游戏。现有集合 \(S=\{1\cdots n\times m+1\}\),两人轮流从 \(S\) 中取出一个数放到序列 \(a\) 的尾部,序列 \(a\) 初始为空.
    \(P(a)\) 表示此时 \(a\) 中含有长度为 \(n+1\) 的递增子序列,或者长度为 \(m+1\) 的递减子序列.
    这个游戏有两个版本
    版本 1 :谁首次满足 \(P(a)\) 谁获胜
    版本 2 :谁首次满足 \(P(a)\) 谁失败
    无论什么版本,一旦某方无法取数,谁失败.
    给定 \(n,m\) ,和游戏版本。SY 和 JJ 都非常聪明。SY 先手,请你告诉他,他是否能赢。

传送门

知识点:博弈论枚举

考虑到这题 \(n\) 极其小,故以 \(n\) 作为突破点:

当确定版本 2 时,\(n\in[0,1]\)

十分显然,\(n=0\)\(m=0\) 时先手一定直接达到 \(P(a)\) ,必败

\(n=1\)\(m=1\) 时,先手选择最小值,后手一定会形成长度为 \(2\) 的上升序列;先手选择最大值,后手一定会形成长度为 \(2\) 的下降序列,必胜

当确定版本 1 时,\(n\in[0,2]\)

十分显然,\(n=0\)\(m=0\) 时先手一定直接达到 \(P(a)\) ,必胜

\(n=1\) 时若某个人选择了一个非可选的最大值,则另一个人一定可以选择之前可选的最大值,凑成 \(2\) 的上升序列。故每次每个人都只会选择可选的最大值,故选择的数将成递减,当 \(M\) 为奇数时,先手必胜。

接下来考虑 \(n=2\)\(m>0\) 的情况。由于可取的数字为 \([1,2m+1]\) ,有奇数个数字。每次先手选择可选数的最小值,则后手必须选择可选数的最大值(否则先手可以再选择可选数的最大值,到达 \(P(a)\) ),故每次操作会减少两个可选数。当仅剩 \(1\) 个数字时,先手取走,故后手无数可取。因此先手必胜。

总结一下获胜情况:

\( win= \begin{cases} \begin{cases} 1,n=0\vee m=0\vee n=2 \\\ \\ [2\nmid m],n=1 \end{cases} ,v=1 \\\ \\ \begin{cases} 0,n=0\vee m=0 \\\ \\ 1,n=1\vee m=1 \end{cases} ,v=2 \end{cases} \)

照这样说,核心代码可以只有一行:

return (v==1)?((n==0||m==0||n==2)?1:(m&1)):(n&(!!m));

【代码】

#include<iostream>
using namespace std;
typedef long long ll;
inline char *ans(ll N,ll M,ll V){
    if(V==1){
        if(N==0||M==0) return "YES";
        if(N==1){
            if(M+1&1) return "YES";
            else return "NO";
        }
        if(N==2) return "YES";
    }
    else{
        if(N==0||M==0) return "NO";
        return "YES";
    }
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    ll T,N,M,V;
    while(cin>>T){
        while(T--&&cin>>N>>M>>V) cout<<ans(N,M,V)<<endl;
    }
    return 0;
}

  1. 排列小球 (FZU/FOJ 2325)

    三种颜色球各 \(n\) 个,混合在一起,排成一行,问有多少种方案使得相邻两个球颜色不同。

传送门

JC 学长:先放入第一种颜色的小球,然后枚举第二种颜色的小球把第一种小球分成几段,接下来会发现有几个位置是必放球的,再乘个组合数就完事了(我随便口胡的)

知识点:排列组合预处理

考虑在一行中先放入第一种小球,接下来考虑第二种小球把第一种小球分为 \(k\) 段。

此时可以想到,将第一种小球分为 \(k\) 段,相当于在 \((n-1)\) 个位置插入 \((k-1)\) 个隔板,方案数为 \(C_{n-1}^{k-1}\)

而第二种小球要将第一种小球分为 \(k\) 段,自身可能是 \((k-1)\) 段(两边均为第一种小球)、 \(k\) 段(两边中的一边为第一种小球,一边为第二种小球)或者 \((k+1)\) 段(两边均为第二种小球)。

我们分别考虑第二种小球被分为 \(t\) 段的情况情况:

由分第一种小球的可知,分法的方案数为 \(C_{n-1}^{t-1}\)

分好后和第一种小球放在一起。当分成 \((k-1)\) 段时第一个一定是第一种小球,故考虑上摆放,方案数为 \(1\cdot C_{n-1}^{k-1}\cdot C_{n-1}^{t-1}\) ;当分成 \(k\) 段时,第一个可能是第一种小球,可能是第二种小球,故方案数为 \(2\cdot C_{n-1}^{k-1}\cdot C_{n-1}^{t-1}\) ;当分成 \((k+1)\) 段时,第一个一定是第二种小球,故方案数为 \(1\cdot C_{n-1}^{k-1}\cdot C_{n-1}^{t-1}\) 。总结一下,方案数即为 \(C_{n-1}^{k-1}\cdot C_{n-1}^{t-1}\cdot (1+[t==k])\)

而在第二种小球分成 \(t\) 段的情况下,两种小球放一起,一共有 \((k+t)\) 段以及 \(2n\) 个球。这种排列模式下,缝隙两侧颜色不同的位置有 \((k+t+1)\) 个:\((k+t)\) 段中有 \((k+t-1)\) 个位置,第一段前面与最后一段后面,加起来 \((k+t+1)\) 个位置。对于剩下的 \([2n+1-(k+t+1)]=(2n-k-t)\) 个位置,两侧颜色相同,一定需要填充第三种小球。

故还剩下 \([n-(2n-k-t)]=(k+t-n)\) 个第三种小球需要填充到前面所说的 \((k+t+1)\) 个位置,方案数为 \(C_{k+t+1}^{k+t-n}\) 。这种情况下,对 \(k\) 段的方案数贡献为 \(C_{n-1}^{k-1}\cdot C_{n-1}^{t-1}\cdot (1+[t==k])\cdot C_{k+t+1}^{k+t-n}\)

稍微简化一下最后一个组合数: \(C_{k+t+1}^{k+t-n}=C_{k+t+1}^{(k+t+1)-(k+t-n)}=C_{k+t+1}^{n+1}\)

当然,这种写法是默认了 \(C_n^m=0,m<0\text{ or }m>n\) 。因为 \(n\) 个小球选负数个,或者选大于 \(n\) 个,显然是不可能的,方案数为 \(0\)

因此我们第一维枚举 \(k\)\(1\)~\(n\),第二维枚举 \(t\)\((k-1)\)~\((k+1)\) 即可求解。

公式为 \(\displaystyle \sum_{k=1}^n\sum_{t=k-1}^{k+1}(1+[t==k])\cdot C_{n-1}^{k-1}\cdot C_{n-1}^{t-1}\cdot C_{k+t+1}^{n+1}\)

有人也许会考虑,是否需要乘上 \(A_3^3\) 枚举排列。其实是不需要的。举个例子表示:

假设考虑的颜色为红黄蓝,分别对应第一种小球、第二种小球、第三种小球的颜色。枚举考虑红球分为 \(k\) 段的时候,我们考虑了黄球分为 \((k-1)\)~\((k+1)\) 段时的情况;如果乘上 \(A_3^3\) ,相当于枚举考虑黄球分为 \((k-1)\)~\((k+1)\) 段时,又分别计算了一次红球分为 \(k\) 段的贡献,枚举重复。同理考虑蓝球,即可得知,不必乘上 \(A_3^3\)

至于组合数,利用公式 \(C_n^m={n!\over m!(n-m)!}\) ,预处理 \(n!\bmod MOD\) 与其逆元,即可 \(O(1)\) 得到结果。

【代码】

#include<iostream>
using namespace std;
typedef long long ll;
const ll MOD=998244353,MAXN=3e6+10;

ll Frac[MAXN],InF[MAXN];
inline ll C(ll n,ll m){//组合数
    if(m<0||m>n) return 0;
    return Frac[n]*InF[m]%MOD*InF[n-m]%MOD;
}
inline ll fpow(ll a,ll x){//快速幂
    ll ans=1;
    for(;x;x>>=1,a=a*a%MOD) if(x&1) ans=ans*a%MOD;
    return ans;
}
inline void pre(){
    Frac[0]=1;
    for(int i=1;i<=3e6;i++) Frac[i]=Frac[i-1]*i%MOD;
    InF[int(3e6)]=fpow(Frac[int(3e6)],MOD-2);
    for(int i=3e6;i>=1;i--) InF[i-1]=InF[i]*i%MOD;
    //预处理阶乘极其逆元

    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
}

inline ll ans(ll n){
    ll Ans=0;
    for(int k=1;k<=n;k++)
        for(int t=k-1;t<=k+1;t++)
            Ans+=(1+(t==k))*C(n-1,k-1)*C(n-1,t-1)%MOD*C(k+t+1,n+1)%MOD;
            //不会溢出
    return Ans%MOD;
}

int main(){
    pre();
    ll N;
    while(cin>>N) cout<<ans(N)<<endl;
    return 0;
}

  1. 集合并 (FZU/FOJ 2326)

    对于集合 \(a\) ,定义集合 \(S(a)\) 表示集合 \(a\) 生成的集合,通过以下步骤任意多次:
    初始,\(S(a)=a\)
    若存在 \(x,y\in S(a)\),但是 \(x\oplus y\not\in S(a)\),将其插入到 \(S(a)\)
    现在给定集合 \(a,b\) ,你需要维护一个数据结构,支持以下操作,共 \(m\) 次:
    1 x,表示插入 \(x\) 到集合 \(a\) 中,保证插入之前 \(x\not\in a\)
    2 x,表示插入 \(x\) 到集合 \(b\) 中,保证插入之前 \(x\not\in b\)
    3 x,表示从集合 \(a\) 中删除元素 \(x\) ,保证删除之前 \(x\in a\)
    4 x,表示从集合 \(b\) 中删除元素 \(x\) ,保证删除之前 \(x\in b\)
    5,表示询问:输出 \(|S(a)\cup S(b)|\bmod 998244353\) ,即集合并的元素个数

\(|S|\) 表示集合 \(S\) 的大小。

传送门

知识点:线性基

比较显然,如果把 \(a\) 放入线性基中,那么 \(S(a)\) 即表示该线性基能构成的所有数字。假设线性基中有 \(cnt\) 个基底,构成的数字个数为 \(2^{cnt}\)

关于操作 \(5\) ,不好正面求出,故我们考虑容斥原理: \(|S(a)\cup S(b)|=|S(a)|+|S(b)|-|S(a)\cap S(b)|\) 。 若 \(S(a)\) 中有 \(cnt_1\) 个基底, \(S(b)\) 中有 \(cnt_2\) 个基底,则 \(|S(a)|,|S(b)|\) 的求法如上所示。而对于 \(S(a)\cap S(b)\) ,显然它的所有基底一定是 \(S(a),S(b)\) 所共有的,故我们将 \(S(a),S(b)\) 的基底求并集,设得到 \(cnt_3\) 个基底,则根据容斥原理, \(S(a)\cap S(b)\) 的基底个数为 \((cnt_1+cnt_2-cnt_3)\) ,剩余的求法如上。

现在考虑前 \(4\) 个操作:显然就是线性基的插入、删除;线性基初始元素可以视为也是线性基的插入,只不过时间更早而已。

但是,学过线性基的各位一定会记得,“正常的”线性基是无法删除的。故我们将线性基进行一些奇妙的操作:

设当前线性基为 \(lb\) ,现在往当中插入一个元素为 \(val\) ,已知这个元素被删除的时间为 \(time\) 。(注:若某元素被插入后删除,再插入等值元素时,视为另一元素。)

那么,我们访问线性基,若访问到某处时,线性基的当前基底不存在,那么插入这个元素就是有效的。我们将这个元素 \(val\) 记录为基底,并标记它的删除时间 \(time\)

若访问到某处,线性基的当前基底存在,且删除时间晚于 \(time\) ,则当成普通线性基处理:异或上当前基底,继续往下执行。

若访问完某处,插入的元素被更换为 \(0\) ,则代表这个元素可以被当前线性基构成,且所有基底被删除的时间都晚于当前元素。故如果后期出现删除该元素的操作,相当于删除了原集合中的该元素,但该元素本身可被线性基构成,故对答案没有影响。即删除非基底元素,对线性基没有影响。

若访问到某处,线性基的当前基底存在,且删除时间早于 \(time\) ,则贪心一下可知,显然这个基底与 \(val\) 交换不会更差:

  1. \(val\) 可被线性基构成,则用 \(val\) 替换任意它能访问的基底都不会对线性基能构成的元素产生变化。且用更后删除的元素替换了更前面删除的基底,则可保证删除原基底的时候,因为已经更换为新基底,故原基底变为非基底,所以线性基不变。
  2. \(val\) 不可被线性基构成,则交换 \(val\) 与该基底后,线性基发生变化:无法构成原基底。而交换后等效于插入原基底,所以最终线性基的效果,和直接插入 \(val\) 没有区别。

在这种插入模式下,我们删除时,若删除的时间为 \(time\) ,则只需要删除应该不晚于 \(time\) 被删除的基底:

  1. 若删除的是非基底元素,由上可知,对线性基没有影响,此时线性基中没有基底早于这个时间,线性基不变。
  2. 若删除的是基底元素,则其被删除的时间应不晚于 \(time\) ,此时的时间进行删除,对线性基产生的影响等价于删除该基底产生的影响。

因此,我们只需要知道每个插入元素被删除的时间即可完成线性基的插入与删除操作。故我们离线数据,获取每个元素被删除的时间,即可实现。

【代码】

#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const ull MAXN=4e5+10,MOD=998244353,INF=0x3f3f3f3f;

struct LinerBasis{
    ull Num[63],Time[63],Cnt;
};
inline void clear(LinerBasis &lb){//清空线性基
    memset(lb.Num,0,sizeof(lb.Num));
    memset(lb.Time,0,sizeof(lb.Time));
    lb.Cnt=0;
}
inline void insert(LinerBasis &lb,ull val,ull time){//向线性基 lb 中插入被删除时间为 time 的元素 val
    for(int i=62;i>=0;i--)
        if(!val) break;
        else if( (val>>i)&1 ) {
            if(!lb.Num[i]){
                lb.Num[i]=val;
                lb.Time[i]=time;
                lb.Cnt++;
                return ;
            }
            if(lb.Time[i]<time){
                swap(lb.Time[i],time);
                swap(lb.Num[i],val);
            }
            val^=lb.Num[i];
        }
}
inline void erase(LinerBasis &lb,ull val,ull time){//从线性基 lb 中删除被删除时间为 time 的元素 val
    for(int i=62;i>=0;i--)
        if(lb.Num[i]&&lb.Time[i]<=time){
            lb.Num[i]=lb.Time[i]=0;
            lb.Cnt--;
        }
}

struct Command{
    ull Option,EndTime,Number;
}Cmd[MAXN];//记录操作、被删除的时间、数字本身
ull P,Q,M;
LinerBasis lb[3];
map<ull,ull> CmdID[3];//记录该线性基中插入某数字的最近一条指令的ID

inline void insert(const Command &cmd){
    insert(lb[cmd.Option],cmd.Number,cmd.EndTime);//向对应线性基中插入元素
    insert(lb[0],cmd.Number,cmd.EndTime);//向两线性基基底并集的线性基中插入元素
}
inline void erase(const Command &cmd){//同上
    erase(lb[cmd.Option-2],cmd.Number,cmd.EndTime);
    erase(lb[0],cmd.Number,cmd.EndTime);
}

inline void clear(){//清空数据
    memset(Cmd,0,sizeof(Cmd));
    for(int i=0;i<3;i++)
        clear(lb[i]),CmdID[i].clear();
}
inline void work(){
    clear();
    for(int i=1;i<=P;i++){
        cin>>Cmd[i].Number;
        Cmd[i].Option=1;
        Cmd[i].EndTime=INF;//初始化被删除时间为无穷大
        CmdID[1][ Cmd[i].Number ]=i;//第一个线性基中,最近一次插入数字 num 为第 i 条指令
    }
    for(int j=1,i=P+1;j<=Q;j++,i++){
        cin>>Cmd[i].Number;
        Cmd[i].Option=2;
        Cmd[i].EndTime=INF;
        CmdID[2][ Cmd[i].Number ]=i;
    }
    cin>>M;
    for(int j=1,i=P+Q+1;j<=M;j++,i++){
        cin>>Cmd[i].Option;//读入指令
        if(Cmd[i].Option==5) continue;
        cin>>Cmd[i].Number;
        if(Cmd[i].Option<=2){//插入操作
            Cmd[i].EndTime=INF;
            CmdID[ Cmd[i].Option ][ Cmd[i].Number ]=i;
            continue;
        }
        Cmd[ CmdID[ Cmd[i].Option-2 ][ Cmd[i].Number ] ].EndTime=i;//删除操作时,被删除的线性基是第 (opt-2) 个
        Cmd[i].EndTime=i;
    }

    for(int i=1,I=P+Q+M;i<=I;i++)
        if(Cmd[i].Option==5)
            cout<<( (1ull<<lb[1].Cnt)%MOD + (1ull<<lb[2].Cnt)%MOD - (1ull<<lb[1].Cnt+lb[2].Cnt-lb[0].Cnt)%MOD + MOD )%MOD<<endl;
        else if(Cmd[i].Option<=2)
            insert(Cmd[i]);
        else
            erase(Cmd[i]);
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    while(cin>>P>>Q) work();
    return 0;
}

std:

#include<iostream>
#include<map>
#include<vector>
#include<cstring>
#define PI acos(-1)
#define pb push_back
#define all(x) x.begin(),x.end()
#define INF 0x3f3f3f3f
#define dd(x) cout<<#x<<" = "<<x<<","
#define de(x) cout<<#x<<" = "<<x<<"\n"
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int N=6e5+5;
const int M=63;
const int mod=998244353;
struct node{
	int op,t;
	ll x;
};
node q[N];
ll qpow(ll a,ll n){
	ll re=1;
	while(n>0){
		if(n&1){
			re*=a;
			re%=mod;
		}
		a*=a;
		a%=mod;
		n>>=1; 
	}
	return re;
}
map<ll,int> pre[3];
ll a[N],b[N],c[M][4];
int cnt[4];
int t[M][4];
void init() {
	for(int i = 0; i < 3; i++)pre[i].clear();
	memset(a, 0, sizeof a); memset(b, 0, sizeof b); memset(c, 0, sizeof c);
	memset(cnt, 0, sizeof cnt);
	memset(t, 0, sizeof t);
}
void ins(ll x,int time,int num){
	for(int i=M-1;i>=0;i--){
		if(x==0){
			break;
		}
		else if(x&(1ll<<i)){
			if(c[i][num]==0){
				c[i][num]=x;
				t[i][num]=time;
				cnt[num]++;
				break;
			}
			else{
				if(t[i][num]<time){
					swap(x,c[i][num]);
					swap(time,t[i][num]);
				}
				x^=c[i][num];
			}
		}
	}
}
void del(int time,int num){
	for(int i=M-1;i>=0;i--){
		if(c[i][num]!=0&&t[i][num]<=time){
			c[i][num]=t[i][num]=0;
			cnt[num]--;
		}
	}
}
int solve(int n, int m) {
	init();
	for(int i=1;i<=n;i++){
		scanf("%I64d",&a[i]);
		q[i].op=1;q[i].t=INF;q[i].x=a[i];pre[1][a[i]]=i;
	}
	for(int i=n+1;i<=n+m;i++){
		scanf("%I64d",&b[i]);
		q[i].op=2;q[i].t=INF;q[i].x=b[i];pre[2][b[i]]=i;
	}
	int t;
	scanf("%d",&t);
	for(int i=n+m+1;i<=n+m+t;i++){
		scanf("%d",&q[i].op);
		if(q[i].op!=5){
			scanf("%I64d",&q[i].x);
			if(q[i].op<=2){
				pre[q[i].op][q[i].x]=i;
				q[i].t=INF;
			}
			else if(q[i].op<=4){
				q[i].op-=2;
				q[pre[q[i].op][q[i].x]].t=i;
				q[i].op+=2;
			}
		}
	}
	for(int i=1;i<=n+m+t;i++){
		if(q[i].op<=2){
			ins(q[i].x,q[i].t,q[i].op);
			ins(q[i].x,q[i].t,3);
		}
		else if(q[i].op<=4){
			q[i].op-=2;
			del(i,q[i].op);
			del(i,3);
		}
		else if(q[i].op==5){
			printf("%I64d\n",((qpow(2,cnt[1])+qpow(2,cnt[2])-qpow(2,cnt[1]+cnt[2]-cnt[3]))%mod+mod)%mod);
		}
	}
}
int main()
{
	int n,m;
	while(scanf("%d%d",&n,&m)!=EOF)solve(n, m);
	
}
posted @ 2020-07-23 00:22  JustinRochester  阅读(1852)  评论(2编辑  收藏  举报