亿些防不住 AK 的水题(Part I)

请忽视标题并膜 GaryH \(\aleph_0\) 分钟

CF1580D Subsequence

题意

有一个长为 \(n\) 的数组 \(a\)。其中令 \(a\) 的一个长为 \(m\) 的子序列为 \(\{a_{b_1},a_{b_2},\cdots,a_{b_m}\}\),其中 \(1\le b_1,b_2,\cdots,b_m\le n\)\(m\) 为定值,则定义其价值为

\[\sum_{i=1}^m(m\cdot a_{b_i})-\sum_{i=1}^m\sum_{j=1}^m\min_{k=\min(b_i,b_j)}^{\max(b_i,b_j)}a_k \]

求价值最大的子序列的价值。\(1\le m<n\le 4000,a_i\le 2^{31}\)

解法

此题需要统计大量区间最值,考虑笛卡尔树。

回顾一下笛卡尔树的性质:

其每个节点有两个权值 \(\{a_i,b_i\}\),其中树上的 \(b_i\) 满足二叉搜索树的性质,\(a_i\) 满足堆的性质。若 \(b_i\) 连续,则某棵子树一定可以对应一个连续的区间,以 \(b_i\) 为下标。假定 \(a_i\) 满足小根堆的性质,令子树的根结点为 \(p\),将非 \(p\) 右子树内的 \(b_l\) 作为子区间左端点,将非 \(p\) 左子树内的 \(b_r\) 作为子区间右端点,则 \(\min_{i=b_l}^{b_r} a_i\) 一定为 \(a_p\),也就是 \(a_{\operatorname{lca}(l,r)}\)

对题中 \(a\) 建立小根笛卡尔树,则此时 \(\min_{k=\min(b_i,b_j)}^{\max(b_i,b_j)}a_k\) 即可换为 \(a_{\operatorname{lca}(b_i,b_j)}\)。考虑对笛卡尔树进行树形 dp,则可以根据根结点合并两子树的信息,也就是两区间最优子序列的信息。

具体来讲,令 \(T_i\) 表示以 \(i\) 结点为根的子树,\(ls_i,rs_i\) 分别代表 \(i\) 结点的左右自结点,设 \(dp_{i,j}\) 表示以 \(i\) 结点为根的子树代表的区间中选长为 \(j\) 的子序列的最大 \(\sum_{p\in T_i} (m\cdot a_{b_p})-\sum_{p,q\in T_i} a_{\operatorname{lca}(b_p,b_q)}\),则转移方程有:

\[dp_{i,j}=\max_{k=0}^{j}(dp_{ls_i,k}+dp_{rs_i,j-k}-2k(j-k)a_i) \]

\[dp_{i,j+1}=\max_{k=0}^{j}(dp_{ls_i,k}+dp_{rs_i,j-k}+ma_i-2(k+1)(j-k+1)a_i) \]

时间复杂度看似为 \(\mathcal{O}(\sum_{i=1}^n \min(|T_{ls_i}|+1,m)\min(|T_{rs_i}|+1,m))=\mathcal{O}(nm^2)\) 的,但是由于 \((|T_{ls_i}|+1)(|T_{rs_i}|+1)\) 代表包括 \(i\) 的一段区间,且保证了每个区间 \([l,r]\) 只会在 \(\operatorname{lca}(l,r)\) 处对时间复杂度有一次贡献,则时间复杂度实则为 \(\mathcal{O}(n^2)\) 的。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
int m,n,i,siz[4010];
long long a[4010],dp[4010][4010];
int build(const int l,const int r){
    if(l>r) return 0;
    int t=l,b,c,xb,xc,ls,rs;
    for(b=l;b<=r;++b) if(a[b]<a[t]) t=b;
    ls=build(l,t-1);rs=build(t+1,r);
    siz[t]=siz[ls]+siz[rs]+1;
    xb=min(siz[ls],m);
    xc=min(siz[rs],m);
    dp[t][0]=0;
    for(b=0;b<=xb;++b){
        for(c=0;c<=xc;++c){
            dp[t][b+c]=max(dp[t][b+c],dp[ls][b]+dp[rs][c]-a[t]*b*c*2);
            dp[t][b+c+1]=max(dp[t][b+c+1],dp[ls][b]+dp[rs][c]+a[t]*(m+1-2*(b+1)*(c+1)));
        }
    }
    return t;
}
int main(){
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;++i) scanf("%d",a+i);
    memset(dp,0xcf,sizeof(dp));
    dp[0][0]=0;
    printf("%lld",dp[build(1,n)][m]);
    return 0;
}

CF283E Cow Tennis Tournament

有一张 \(n\) 个点的有向图,最初 \(\forall i,j\in[1,n]\cap i<j\),从 \(i\)\(j\) 连有一条有向边。同时有 \(m\) 次操作,每次操作会选取一段区间 \([l,r]\),对于 \(\forall i,j\in[l,r]\),将 \(i\)\(j\) 间连边反向。求最后有多少个三元环。\(n,m\le 10^5\)

解法

最开始我在如何进行有向边翻转形成三元环上走了不少弯路,所以下面将提供一种更为便捷的思路。

显然任意三个点之间都满足两两连边。而对于这三个点不能形成三元环的情况,一定有且只有一个点向另外两个点连边。若将点 \(i\) 视为上述的点,其出度为 \(d_i\),则对于有 \(i\) 在内的三个点,一定多有 \(C_{d_i}^2\) 种组合不满足三元环。而选择三个点的选法有 \(C_n^3\) 种,则 \(C_n^3-\sum_{i=1}^nC_{d_i}^2\) 即为答案。

具体维护区间操作可以用扫描线。将区间左右端点一起排序,然后用线段树维护 \(i\) 点连向了哪些点,将信息整合为 \(01\) 序列 \(B\)。具体地,若 \(\exists j\ne i\) 满足 \(i\)\(j\) 间的边被翻转,则 \(B_j\)\(1\),否则 \(B_j\)\(0\)。在由 \(i\) 点推到 \(i+1\) 点时,若某些区间右端点为 \(i\) 或左端点为 \(i+1\),则翻转这些区间。

时间复杂度: \(\mathcal{O}(m\log(n+m)+n\log n)\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
int n,k,i,t,l,r,tot,lst;
int a[maxn],stk[maxn*3],lt[maxn],rt[maxn];
long long s,ans;
struct node{
    int idx,val;
    inline bool operator <(const node &a)const{
        if(val!=a.val) return val<a.val;
        return idx&&!a.idx;
    }
}N[maxn*3];
struct seg{
    int l,r,m,sum,len;
    bool tag;
}tr[maxn<<2];
#define l(p) tr[p].l
#define r(p) tr[p].r
#define m(p) tr[p].m
#define ls(p) p<<1
#define rs(p) p<<1|1
#define len(p) tr[p].len
#define sum(p) tr[p].sum
#define tag(p) tr[p].tag 
inline void Pushup(const int &p){
    sum(p)=sum(ls(p))+sum(rs(p));
} 
void Build(const int p,const int la,const int ra){
    l(p)=la;r(p)=ra;
    m(p)=(la+ra)>>1;
    len(p)=ra-la+1;
    if(la==ra) return;
    Build(ls(p),la,m(p));
    Build(rs(p),m(p)+1,ra);
    Pushup(p);
}
inline void Pushdown(const int &p){
    if(!tag(p)) return;
    tag(ls(p))^=1;tag(rs(p))^=1;
    sum(ls(p))=len(ls(p))-sum(ls(p));
    sum(rs(p))=len(rs(p))-sum(rs(p));
    tag(p)=0; 
}
void Xor(const int p,const int &la,const int &ra){
    if(la<=l(p)&&ra>=r(p)){
        sum(p)=len(p)-sum(p);
        tag(p)^=1;return;
    }
    Pushdown(p);
    if(la<=m(p)) Xor(ls(p),la,ra);
    if(ra>m(p)) Xor(rs(p),la,ra);
    Pushup(p);
}
void Query(const int p,const int &la,const int &ra){
    if(la<=l(p)&&ra>=r(p)){
        s+=sum(p);
        return;
    } 
    Pushdown(p);
    if(la<=m(p)) Query(ls(p),la,ra);
    if(ra>m(p)) Query(rs(p),la,ra);
}
int main(){
    scanf("%d%d",&n,&k);
    ans=(1LL*n*(n-1)*(n-2))/6;
    for(i=1;i<=n;++i){
        scanf("%d",&l); 
        N[i].val=a[i]=l;
    } 
    sort(a+1,a+n+1);
    tot=n;
    for(i=1;i<=k;++i){
        scanf("%d%d",&l,&r);
        if(l>=a[n]) continue;
        lt[i]=lower_bound(a+1,a+n+1,l)-a;
        rt[i]=upper_bound(a+1,a+n+1,r)-a-1;
        if(++r<=a[1]) continue;
        l=a[lt[i]];
        if(r>a[n]) r=a[n]+1;
        else r=*lower_bound(a+1,a+n+1,r); 
        N[++tot]={i,l};N[++tot]={i,r};
    }
    sort(N+1,N+tot+1);
    lst=N[1].val;
    N[1].val=r=1;
    for(i=2;i<=tot;++i){
        while(N[i].val==lst) N[i++].val=r;
        if(i>tot) break;
        lst=N[i].val;N[i].val=++r;
    }
    Build(1,1,n);
    for(i=1;i<=tot;++i){
        r=N[i].val;
        if(l=N[i].idx) stk[++t]=l;
        else{
            while(t){
                r=stk[t--];
                Xor(1,lt[r],rt[r]);
            }
            if(N[i].val>1){
                Query(1,1,N[i].val-1);
                s=N[i].val-1-s;
            }
            if(N[i].val<n) Query(1,N[i].val+1,n);
            ans-=(s*(s-1))>>1;s=0;
        }
    }
    printf("%lld",ans);
    return 0;
}

CF724E Goods Transportation

听说 GaryH 秒切这道 *2900 的题

题意

\(n\) 个商店,第 \(i\) 个商店初始有 \(a_i\) 个商品,最多卖出 \(b_i\) 个商品。若 \(\exists i,j\in[1,n],i<j\) ,则最多从 \(i\)\(c\) 个物品到 \(j\)。求最多卖出商品数。\(n\le 4000\)

解法

有一个朴素的网络流做法:建立超级源点 \(S\)\(T\)。对于 \(i\) 点,从 \(S\)\(i\) 连一条边权为 \(a_i\) 的边,从 \(i\)\(T\) 连一条边权为 \(b_i\) 的边。对于 \(\forall i\ne j\),从 \(\min(i,j)\)\(\max(i,j)\) 连一条边权为 \(c\) 的边。最大流即为答案。然而其时间复杂度高达 \(\mathcal{O}(n^4)\)

考虑最大流转为最小割。(对于长得很别致的图,常常可以考虑模拟最小割。)最小割中 \(\forall i\) 一定与 \(S\)\(T\) 相连。从小到大考虑 \(i\) 点,设 \(dp_{i,j}\) 为前 \(i\) 个点共有 \(j\) 个点与 \(S\) 相连的答案。转移即有:

\[dp_{i,j}=\min(dp_{i-1,j}+a_i+jc,dp_{i-1,j-1}+b_i) \]

时间复杂度:\(\mathcal{O}(n^2)\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
int n,i,j;
long long f[10010],p[10010],s,c,r,t;
int main(){
    memset(f,0x3f,sizeof(f));
    scanf("%d%lld",&n,&c); 
    f[0]=0;
    for(i=1;i<=n;++i) scanf("%lld",p+i);
    for(i=1;i<=n;++i){
        scanf("%lld",&s);t=p[i]+(r+=c);
        for(j=i;j;--j) f[j]=min(f[j]+t,f[j-1]+s),t-=c;
        f[0]+=p[i];
    }
    printf("%lld",*min_element(f,f+n+1));
    return 0;
}

CF1368H1 Breadboard Capacity (easy version)

CF1368H2 Breadboard Capacity (hard version)

听说 tourist 都没有时间做掉 hard version,然后这个题成为了某场考试的 T2,plate_let 做完 hard version 都耗时三个半小时

题意

有一个 \(n*m\) 的网格,其四边上有 \(2n+2m\) 个红色或蓝色的接口。现在需要给一部分红色接口配对上一个尚未配对的蓝色接口,对应给网格内接若干导线,满足:

  1. 两个相邻的点最多只有一条导线。
  2. 配对的两个接口必须要经过若干条导线组成的路径相连。
  3. 上述的路径不能共用导线。

求最多配对的接口数。

加强版中:给出 \(q\) 次修改,每次选择网格一条边一个区间,将接口换为不同颜色的,并在每次修改后询问上述问题。

\(n,m,q\le 10^5\)

解法

考虑网络流做法:

对整个网格每对相邻位置建边权为 \(1\) 的双向边,将源点 \(S\) 向每个红色接口,每个蓝色接口向汇点 \(T\) 连边权为 \(\inf\) 的单向边。最大流即为答案。

这样做的点数和边数高达 \(\mathcal{O}(nm)\)

考虑 CF724E Goods Transportation 的做法:利用最大流=最小割的性质模拟最小割。但是本题中我们建了大量的双向边,看似不能保证求出的最大流每条边最多只被流经一次(最小割每条边只会被考虑一次),进而建立模拟最小割模型。考虑如下转化:

被经过两次的双向边可以在不改变答案的前提下改为不被经过,从而确保了每对点的边在割掉时贡献只会被计算一次。

考虑割的标准定义:将与 \(S\)\(T\) 连通的点(包括 \(S\)\(T\))划分为两个点集 \(V_S\)\(V_T\),满足 \(V_S=\{S\}\cup\{\texttt{一部分 S 能到达的点}\}\)\(V_T=\{T\}\cup\{\texttt{一部分能到达 T 的点}\}\),定义这样划分的价值(即割的权值和)为 \(\sum_{\vec e=(u\rightarrow v)(u\in V_S,v\in V_T)}|\vec e|\)。所有划分的最小价值即为最小割的权值和。

对此题进行上述划分后,则红色接口只会在 \(V_S\) 内,蓝色接口只在 \(V_T\) 内。下面为了方便,会把 \(V_S\) 内的点表示为红色,\(V_T\) 内的点表示为蓝色,将割掉边后相连的点集记为连通块。异色连通块的边界总长(不包括网格四周的)即为割边总数。

定理:最优方案中网格内连通块一定与至少一组对边相连且为矩形。

证明:

若某连通块不与周围任意一边相连,则将其变成另一种颜色后割边变少,一定更优。

同时若某连通块只与一条边相连,同样可以将其变为另一种颜色,减少的割边数一定多于增加的,答案也会更优。

同理,这样的只与一组相邻边界的连通块也可以反色,减少的割边数不会少于增加的。

并且,连通块凸出的边界可以反色,凸型的边界也可以推进到对边上(或是进行收缩),可以发现答案始终不会更劣。

由此,我们完成了定理的证明,即每行或每列必须同色。考虑每列同色时,设 \(dp_{i,X}(X\in[0,1])\) 表示考虑到第 \(i\) 行染色为 \(X\) (令 \(0\) 为红色,\(1\) 为蓝色),转移方程如下:

\[dp_{i,A}=\min(dp_{i-1,A},dp_{i-1,!A}+n)+[U_i=A]+[D_i=A] \]

注意初值 \(dp_{1,A}=[U_1=A]+[D_i=A]+\sum_{i=0}^n[L_i=A]\),最后 \(dp_{m,A}\leftarrow dp_{m,A}+\sum_{i=0}^n[R_i=A]\)

其中 \(L\)\(R\)\(U\)\(D\) 分别为左右两列,上下两行的接口颜色序列。

对每行同色时采取类似方法。总复杂度为 \(\mathcal{O}(n+m)\)

加强版:考虑线段树维护矩阵乘法。上述式子 \(dp_{i,A}=\min(dp_{i-1,A}+[U_i=A]+[D_i=A],dp_{i-1,!A}+n+[U_i=A]+[D_i=A])\) 可以看作广义矩阵乘法的形式:

\[\begin{bmatrix}dp_{i,0}&dp_{i,1}\end{bmatrix}\times\begin{bmatrix}!U_i+!D_i & n+U_i+D_i\\ n+!U_i+!D_i & U_i+D_i\end{bmatrix}=\begin{bmatrix}dp_{i+1,0}&dp_{i+1,1}\end{bmatrix} \]

其中 \(A\times B=C(C_{i,j}=\min(A_{i,k}+B_{k,j}))\)

证明上述乘法满足结合律:

\(A\)\(P\times M\) 的矩阵,\(B\)\(M\times Q\) 的矩阵,\(C\)\(Q\times R\) 的矩阵,则

\[\begin{aligned}((A\times B)\times C)_{i,j}&=\min_{k=1}^Q((A\times B)_{i,k}+C_{k,j})\\&=\min_{k=1}^Q(\min_{x=1}^M(A_{i,x}+(B_{x,k}+C_{k,j})))\\&=\min_{x=1}^M(A_{i,x}+\min_{k=1}^Q(B_{x,k}+C_{k,j}))\\&=\min_{x=1}^M(A_{i,x}+(B\times C)_{x,j})\\&=(A\times (B\times C))_{i,j}\end{aligned} \]

故而在修改时,使用线段树维护所有可能的 \(4\) 种矩阵(目前状态下不反转颜色,反转一条边的颜色,反转两条边的颜色)和 \(\sum_{i=0}^n L_i\) 等内容,复杂度为 \(\mathcal{O}((n+q)\log n+(m+q)\log m)\),常数有一点大。

代码(似乎有极致压行者代码 2k 左右?)

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
#define ll long long
const ll INF=1145141919810;
int n,m,q,i,lt,rt,th;
char o,s[2][maxn];
ll sr,sb,sR,sB,ans;
struct mat{
    ll xx,xy,yx,yy;
    inline mat operator *(const mat &p){
        return (mat){min(xx+p.xx,xy+p.yx),
                     min(xx+p.xy,xy+p.yy),
                     min(yx+p.xx,yy+p.yx),
                     min(yx+p.xy,yy+p.yy)};
    } 
}tmp;
struct seg{
    int l,r,m,len;
}tr[4][maxn<<2];
#define l(p) Tr[p].l
#define r(p) Tr[p].r
#define m(p) Tr[p].m
#define len(p) Tr[p].len
#define ls(p) p<<1
#define rs(p) p<<1|1
#define sum(p) tr[p].sum
#define tag(p) tr[p].tag
#define rev(p) tr[p].rev
#define tag1(p) tr[p].tag1
#define tag2(p) tr[p].tag2
#define mul(p,c,d) tr[p].mul[c][d]
void Build(const int t,const int p,const int l,const int r){
    tr[t][p]={l,r,(l+r)>>1,r-l+1};
    if(l==r) return;
    Build(t,ls(p),l,(l+r)>>1);
    Build(t,rs(p),((l+r)>>1)+1,r);
}
struct Sums{
    seg *Tr;
    struct segs{
        int sum;
        bool tag;
    }tr[maxn<<2];
    inline void Pushup(const int p){
        sum(p)=sum(ls(p))+sum(rs(p));
    } 
    void Build(const int p,const bool u){
        if(l(p)==r(p)){
            sum(p)=s[u][l(p)];
            return;
        }
        Build(ls(p),u);
        Build(rs(p),u);
        Pushup(p);
    }
    inline void Pushdown(const int p){
        if(!tag(p)) return;
        sum(ls(p))=len(ls(p))-sum(ls(p));
        sum(rs(p))=len(rs(p))-sum(rs(p));
        tag(ls(p))^=1;tag(rs(p))^=1;
        tag(p)=0;
    }
    void Reverse(const int p){
        if(lt<=l(p)&&rt>=r(p)){
            sum(p)=len(p)-sum(p);
            tag(p)^=1;
            return;
        }
        Pushdown(p);
        if(lt<=m(p)) Reverse(ls(p));
        if(rt>m(p)) Reverse(rs(p));
        Pushup(p);
    }
    int QueryL(){
        int p=1;
        while(r(p)!=1){
            Pushdown(p);
            p=ls(p);
        }
        return sum(p);
    }
}S[4];
struct Mats{
    seg *Tr;
    struct segm{
        bool tag1,tag2;
        mat mul[2][2];
    }tr[maxn<<2];
    inline void Pushup(const int &p){
        mul(p,0,0)=mul(ls(p),0,0)*mul(rs(p),0,0);
        mul(p,0,1)=mul(ls(p),0,1)*mul(rs(p),0,1);
        mul(p,1,0)=mul(ls(p),1,0)*mul(rs(p),1,0);
        mul(p,1,1)=mul(ls(p),1,1)*mul(rs(p),1,1);
    }
    void Build(const int p,const int &k){
        if(l(p)==r(p)){
            const int x=l(p);
            int s1=s[0][x]+s[1][x],s2=!s[0][x]+s[1][x];
            mul(p,0,0)=(mat){s1,k+2-s1,k+s1,2-s1};
            mul(p,0,1)=(mat){s2,k+2-s2,k+s2,2-s2};
            mul(p,1,0)=(mat){2-s2,k+s2,k+2-s2,s2};
            mul(p,1,1)=(mat){2-s1,k+s1,k+2-s1,s1};
            return;
        }
        Build(ls(p),k);Build(rs(p),k);
        Pushup(p);
    }
    inline void Swap1(const int &p){
        swap(mul(p,0,0),mul(p,0,1));
        swap(mul(p,1,0),mul(p,1,1));
        tag1(p)^=1;
    }
    inline void Swap2(const int &p){
        swap(mul(p,0,0),mul(p,1,0));
        swap(mul(p,0,1),mul(p,1,1));
        tag2(p)^=1;
    }
    inline void Pushdown(const int &p){
        if(tag1(p)){
            Swap1(ls(p));
            Swap1(rs(p));
            tag1(p)=0;
        }
        if(tag2(p)){
            Swap2(ls(p));
            Swap2(rs(p));
            tag2(p)=0;
        }
    }
    void Reverse(const int p,const bool x){
        if(lt<=l(p)&&rt>=r(p)){
            if(x) Swap2(p);
            else Swap1(p);
            return;
        }
        Pushdown(p);
        if(lt<=m(p)) Reverse(ls(p),x);
        if(rt>m(p)) Reverse(rs(p),x);
        Pushup(p);
    }
}M[2];
inline void Ans(){
    th=S[2].QueryL()+S[3].QueryL();
    sr=S[0].tr[1].sum;
    sb=n-sr+2-th;sr+=th;
    tmp=M[0].tr[1].mul[0][0]; 
    sR=min(sr+tmp.xx,sb+tmp.yx);
    sB=min(sr+tmp.xy,sb+tmp.yy);
    sr=S[1].tr[1].sum;
    sR+=sr;sB+=n-sr;
    ans=min(sR,sB);
    th=S[0].QueryL()+S[1].QueryL();
    sr=S[2].tr[1].sum;
    sb=m-sr+2-th;sr+=th;
    tmp=M[1].tr[1].mul[0][0];
    sR=min(sr+tmp.xx,sb+tmp.yx);
    sB=min(sr+tmp.xy,sb+tmp.yy);
    sr=S[3].tr[1].sum;
    sR+=sr;sB+=m-sr;
    printf("%lld\n",min(ans,min(sR,sB)));
}
int main(){
    scanf("%d%d%d",&n,&m,&q);
    Build(0,1,1,n);
    Build(1,1,1,m);
    if(n>1) Build(2,1,2,n);
    if(m>1) Build(3,1,2,m);
    S[0].Tr=S[1].Tr=tr[0];
    S[2].Tr=S[3].Tr=tr[1];
    M[0].Tr=tr[3];M[1].Tr=tr[2];
    scanf("%s%s",s[0]+1,s[1]+1);
    for(i=1;i<=n;++i){
        if(s[0][i]=='B') s[0][i]=1;
        else s[0][i]=0;
        if(s[1][i]=='B') s[1][i]=1;
        else s[1][i]=0;
    }
    S[0].Build(1,0);
    S[1].Build(1,1);
    if(n>1) M[1].Build(1,m);
    else M[1].tr[1].mul[0][0]={0,INF,INF,0};
    scanf("%s%s",s[0]+1,s[1]+1);
    for(i=1;i<=m;++i){
        if(s[0][i]=='B') s[0][i]=1;
        else s[0][i]=0;
        if(s[1][i]=='B') s[1][i]=1;
        else s[1][i]=0;
    }
    S[2].Build(1,0);
    S[3].Build(1,1);
    if(m>1) M[0].Build(1,n);
    else M[0].tr[1].mul[0][0]={0,INF,INF,0};
    Ans();
    while(q--){
        scanf(" %c%d%d",&o,&lt,&rt);
        if(o=='L'){
            S[0].Reverse(1);
            if(n>1&&rt>1){
                if(lt==1) lt=2;
                M[1].Reverse(1,0);
            }
        }
        else if(o=='R'){
            S[1].Reverse(1);
            if(n>1&&rt>1){
                if(lt==1) lt=2;
                M[1].Reverse(1,1);
            }
        }
        else if(o=='U'){
            S[2].Reverse(1);
            if(m>1&&rt>1){
                if(lt==1) lt=2;
                M[0].Reverse(1,0);
            }
        }
        else{
            S[3].Reverse(1);
            if(m>1&&rt>1){
                if(lt==1) lt=2;
                M[0].Reverse(1,1);
            }
        }
        Ans();
    } 
    return 0;
}

P6667 [清华集训2016] 如何优雅地求和

题意

有一个 \(m\) 次多项式 \(f\),定义变换 \(Q\) 为:\(Q(f,n,x)=\sum_{k=0}^nf(k)C_n^kx^k(1-x)^{n-k}\)。现给出 \(f,n,x\),求 \(Q(f,n,x)\!\!\!\mod 998244353\)

\(n\le 10^9,m\le 2\cdot 10^4\)

解法

听说这个题弄晕了 TyyyyyyRyyyyyyL7_56zhouxp 等一众 IOI AKers

\(f(x)=\sum_{j\ge0}f_jx^{\underline j}\)(将普通幂转下降幂)(可以发现 \(\forall i>m,f_i=0\)),可以将原等式转化为

\[\begin{aligned}&{\color{white}\quad\,}\sum_{k=0}^nf(k)\binom nk x^k(1-x)^{n-k}\\&=\sum_{k=0}^n((\sum_{p\ge0}f_pk^{\underline p})\frac{n!}{k!(n-k)!}x^k(1-x)^{n-k})\\&=\sum_{k=0}^n((\sum_{p=0}^kf_pk^{\underline p})\frac{n!}{k!(n-k)!}x^k(1-x)^{n-k})\\&=\sum_{k=0}^n\sum_{p=0}^k\frac{n!f_pk^{\underline p}}{k^{\underline k}(n-k)!}x^k(1-x)^{n-k}\\&=\sum_{k=0}^n\sum_{p=0}^k\frac{f_p(n-p)!}{(k-p)!(n-k)!}n^{\underline p}x^k(1-x)^{n-k}\\&=\sum_{k=0}^n\sum_{p=0}^kf_pn^{\underline p}x^p\binom{n-p}{k-p}x^{k-p}(1-x)^{n-k}\\&=\sum_{p\ge0}f_pn^{\underline p}x^p\sum_{k=p}^n\binom{(k-p)+(n-k)}{k-p}x^{k-p}(1-x)^{n-k}\\&=\sum_{p\ge0}f_pn^{\underline p}x^p\end{aligned} \]

此时考虑求 \(f_p\),可以构造 \(F(x)=\sum_{i\ge0}\frac{f(i)x^i}{i!}\),此时有

\[\begin{aligned}&{\color{white}\quad\,}\sum_{i\ge0}\frac{f(i)x^i}{i!}\\&=\sum_{i\ge0}\frac{x^i}{i!}\sum_{j\ge0}f_ji^{\underline j}\\&=\sum_{i\ge0}\sum_{j=0}^i\frac{x^i}{i!}f_j\frac{i!}{(i-j)!}\\&=\sum_{i\ge0}\sum_{j=0}^i\frac{x^{i-j}}{(i-j)!}f_jx^j\\&=\sum_{j\ge0}\sum_{i\ge j}\frac{x^{i-j}}{(i-j)!}f_jx^j\\&=\sum_{j\ge0}\sum_{i\ge0}\frac{x^i}{i!}f_jx^j\\&=e^x\sum_{j\ge0}f_jx^j\end{aligned} \]

故而 \(\sum_{i\ge0}f_ix^i=\sum_{i=0}^mf_ix^i=e^{-x}\sum_{i\ge0}\frac{f(i)x^i}{i!}\),由链式求导法则可证明 \((e^{-x})'=-e^{-x}\),(由 \((-e^x)'=-e^x\) 可得)\((e^{-x})''=e^{-x}\),以此类推。通过泰勒展开可得 \(e^{-x}=1+\frac{-1\cdot x^1}{1!}+\frac{1\cdot x^2}{2!}+\frac{-1\cdot x^3}{3!}+\cdots\)。同时由于 \(e^{-x}\sum_{i\ge0}\frac{f(i)x^i}{i!}\) 的最高次数为 \(\sum_{i=0}^mf_ix^i\) 的系数,也就是其不超过 \(m\),所以只需要将 \(e^{-x}=\sum_{i=0}^m\frac{(-1)^i}{i!}x^i\)\(\sum_{i=0}^m\frac{f(i)}{i!}x^i\) 相乘即可求 \(\sum_{i=0}^mf_ix^i\)。时间复杂度为 \(O(m\log m)\)

线性算法可以参考 这篇文章 P6031 的题解部分。

代码

点此查看代码
#include <bits/stdc++.h> 
using namespace std;
#define ll long long
const ll md=998244353;
const ll G=3,InvG=332748118;
const int maxm=32770;
const int maxn=65550;
ll n,x,w,c,d,s,wn,tmp;
ll fac[maxn],inv[maxn];
ll f[maxn],g[maxn];
int m,i,j,t,a,k=-1;
int r[maxn];
inline ll Pow(ll d,int z){
    ll ret=1;
    do{
        if(z&1){
            ret*=d;
            if(ret>=md) ret%=md;
        }
        d*=d;
        if(d>=md) d%=md;
    }while(z>>=1);
    return ret;
}
void DFT(ll *p){
    for(i=1;i<a;++i) if(i<r[i]) swap(p[i],p[r[i]]);
    for(i=k=1;k<a;++i,k<<=1){
        wn=Pow(G,(md-1)>>i);
        for(j=0;j<a;j+=(k<<1)){
            w=1;
            for(t=0;t<k;++t){
                c=p[j+t];d=(w*p[j+t+k])%md;
                p[j+t]=(c+d)%md;
                p[j+t+k]=(c-d+md)%md;
                w=(w*wn)%md;
            }
        }
    }
}
void IDFT(ll *p){
    for(i=1;i<a;++i) if(i<r[i]) swap(p[i],p[r[i]]);
    for(i=k=1;k<a;++i,k<<=1){
        wn=Pow(InvG,(md-1)>>i);
        for(j=0;j<a;j+=(k<<1)){
            w=1;
            for(t=0;t<k;++t){
                c=p[j+t];d=(w*p[j+t+k])%md;
                p[j+t]=(c+d)%md;
                p[j+t+k]=(c-d+md)%md;
                w=(w*wn)%md;
            }
        }
    }
}
int main(){
    scanf("%lld%d%lld",&n,&m,&x);
    fac[0]=inv[0]=1;
    for(i=1;i<=maxm;++i) fac[i]=(fac[i-1]*i)%md;
    inv[maxm]=Pow(fac[maxm],md-2);
    for(i=maxm-1;i;--i) inv[i]=(inv[i+1]*(i+1))%md;
    for(i=0;i<=m;++i){
        scanf("%d",&a);
        g[i]=(inv[i]*a)%md;
        f[i]=inv[i]*(k=-k);
    }
    a=1;k=0;m<<=1;
    while(a<=m){
        a<<=1;
        ++k;
    }
    for(i=0;i<a;++i){
        r[i]=r[i>>1]>>1;
        if(i&1) r[i]|=(1<<(k-1));
    }
    DFT(f);DFT(g);
    for(i=0;i<a;++i) f[i]=(f[i]*g[i])%md;
    IDFT(f);m>>=1;
    c=d=1;w=Pow(a,md-2);
    for(i=0;i<=m;++i){
        f[i]=(f[i]*w)%md;
        s+=((f[i]*c)%md*d)%md;
        d=(d*x)%md;
        c=(c*(n--))%md;
        if(n<0) break;
    }
    printf("%lld",s%md);
}

CF1278F Cards

解法

显然摸出 \(x\) 张王牌的概率为 \(\binom nx(\frac1m)^x(\frac{m-1}m)^{n-x}\)\(x^k\) 的期望值即为 \(\sum_{x=0}^n\binom nx(\frac1m)^x(\frac{m-1}m)^{n-x}x^k\)。然后发现这个式子很 如何优雅地求和。不过这里 \(k\le 5000\) 所以直接暴力卷积即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5010;
const int md=998244353;
int n,m,k,i,j,x,y=1,z=1,ans;
int a[maxn],b[maxn];
inline int Pow(int d,int z){
	int r=1;
	do{
		if(z&1) r=(1LL*d*r)%md;
		d=(1LL*d*d)%md;
	}while(z>>=1);
	return r;
}
int main(){
	a[0]=1;
	for(i=1;i<maxn;++i) a[i]=(1LL*a[i-1]*i)%md;
	b[maxn-1]=Pow(a[maxn-1],md-2);
	for(i=maxn-1;i;--i) b[i-1]=(1LL*b[i]*i)%md;
	scanf("%d%d%d",&n,&m,&k); m=Pow(m,md-2);
	for(i=0;i<=k;++i){
		a[i]=(1LL*Pow(i,k)*b[i])%md;
		if(i&1) b[i]=-b[i];
	}
	for(i=0;i<=k;++i){
		for(x=j=0;j<=i;++j) x=(1LL*a[j]*b[i-j]+x)%md;
		ans=((1LL*x*y)%md*z+ans)%md;
		y=(1LL*y*(n-i))%md; z=(1LL*z*m)%md;
	}
	printf("%d",ans);
	return 0;
}

LOJ P3188 (ROI 2019 Day1)无人驾驶出租车

题意

有一个 \(n\times m\) 的地面,每一时刻每一格地面会多积累 \(1\) 单位厚度积雪。每一时刻会有如下事情之一发生:

  • 某一行的积雪厚度变为 \(0\)
  • 某一列的积雪厚度变为 \(0\)
  • 询问能经过积雪厚度小于 \(k\) 的车能否从 \((sx,tx)\)\((sy,ty)\),若能经过则求其最短路。

\(n,m\le 10^6,q\le 3\cdot 10^5\)

解法

听说这道题省选组考试全员 AC,然而蒟蒻做了好长时间最后还忘了一个 Corner Case

考虑在 \(i\) 时刻的询问操作时 \(k\) 值,对应的能够通行的格子的分布规律。显然其需要在 \(i-k\) 时刻或之后被清楚过积雪。

对应具体的操作,可以发现某一行或某一列在 \(i-k\) 时刻或以后被清除积雪,则此时其一定通行。将这些行和列取并,可以发现最后能通行的区域构成了一个网格。

记某个坐标 \((x,y)\)\(c_{x,y}\) 为:不能通行的为 \(0\),只在能通行的行的为 \(1\),只在能通行的列的为 \(2\),在能通行的行和列的交点的为 \(3\)。下面只考虑 \(c\) 值不为零的坐标情况。

显然,若某两个点的 \(c\) 值不相同,则它们的最短距离即曼哈顿距离。下面考虑两个点只在某一行的情况:

  • 若这两个点中间有一可通行列,则距离为曼哈顿距离。
  • 若两点中间的行均是可通行行,则距离为曼哈顿距离。否则,若无可通行列,则两点不相通。
  • 若两点左/右侧均为同一可通行列,则需要统计两种走法的路程。特别地,若两点均在某边界与一可通行列之间,则只用统计一种走法的路程。

对应用线段树维护最值,则本题需要统计区间 \(\min\),区间 \(\max\),可以用线段树二分(可做到单次 \(O(\log n)\))求对于 \(i\)\(\min_{j\ge i,a_j\ge k}j,\max_{j\le i,a_j\ge k}j\) 等信息。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1000010;
int n,m,q,r,c,i,k,o,ds;
int rs,cs,rt,ct,la,ra,lb,rb;
bool ta,tb;
inline int Abs(const int &p){return p>0?p:-p;}
#define INF 1e9
struct Seg{
    int v[maxn];
    struct node{
        int l,r,m,mx,mn;
    }tr[maxn<<2];
    #define l(p) tr[p].l
    #define r(p) tr[p].r
    #define m(p) tr[p].m
    #define ls(p) p<<1
    #define rs(p) p<<1|1
    #define mx(p) tr[p].mx
    #define mn(p) tr[p].mn
    void build(const int p,const int l,const int r){
        l(p)=l;r(p)=r;
        m(p)=(l+r)>>1;
        if(l==r) return;
        build(ls(p),l,m(p));
        build(rs(p),m(p)+1,r);
    } 
    inline void pushup(const int &p){
        mx(p)=max(mx(ls(p)),mx(rs(p)));
        mn(p)=min(mn(ls(p)),mn(rs(p)));
    }
    void change(const int p,const int x,const int d){
        if(l(p)==r(p)){
            mx(p)=mn(p)=d;
            return;
        }
        if(x<=m(p)) change(ls(p),x,d);
        else change(rs(p),x,d);
        pushup(p);
    } 
    int queryr(const int p,const int r){
        if(mx(p)<k) return 0;
        if(l(p)==r(p)) return l(p);
        int ret=0;
        if(r>m(p)) ret=queryr(rs(p),r);
        if(ret) return ret;
        return queryr(ls(p),r);
    }
    int queryl(const int p,const int l){
        if(mx(p)<k) return 0;
        if(l(p)==r(p)) return l(p);
        int ret=0;
        if(l<=m(p)) ret=queryl(ls(p),l);
        if(ret) return ret;
        return queryl(rs(p),l);
    }
    int queryn(const int p,const int l,const int r){
        if(l<=l(p)&&r>=r(p)) return mn(p);
        int ret=INF;
        if(l<=m(p)) ret=queryn(ls(p),l,r);
        if(r>m(p)) ret=min(ret,queryn(rs(p),l,r));
        return ret;
    }
}T[2];
int main(){
    scanf("%d%d%d",&n,&m,&q);
    T[0].build(1,1,n);
    T[1].build(1,1,m);
    for(i=1;i<=q;++i){
        scanf("%d",&o);
        if(o==1){
            scanf("%d",&rs);
            T[0].v[rs]=i;
            T[0].change(1,rs,i);
        }
        else if(o==2){
            scanf("%d",&cs);
            T[1].v[cs]=i;
            T[1].change(1,cs,i);
        }
        else{
            scanf("%d%d%d%d%d",&rs,&cs,&rt,&ct,&k);
            ds=Abs(rs-rt)+Abs(cs-ct); 
            if(k>=i){
                printf("%d\n",ds);
                continue;
            }
            k=i-k;
            if(max(T[0].v[rs],T[1].v[cs])<k||
               max(T[0].v[rt],T[1].v[ct])<k){
                printf("-1\n");
                continue;
            }
            if(max(T[0].queryn(1,min(rs,rt),max(rs,rt)),
                   T[1].queryn(1,min(cs,ct),max(cs,ct)))>=k){
                printf("%d\n",ds);
                continue;
            }
            la=T[0].queryr(1,rs);ta=1;
            if(la==rs){
                la=T[1].queryr(1,cs);
                if(la==cs){
                    printf("%d\n",ds);
                    continue;
                }
                ta=0;
                ra=T[1].queryl(1,cs);
            }
            else ra=T[0].queryl(1,rs);
            lb=T[0].queryr(1,rt);tb=1;
            if(lb==rt){
                lb=T[1].queryr(1,ct);
                if(lb==ct){
                    printf("%d\n",ds);
                    continue;
                }
                tb=0;
                rb=T[1].queryl(1,ct);
            }
            else rb=T[0].queryl(1,rt);
            if(!(la||ra)){
                printf("-1\n");
                continue;
            }
            if(ta^tb||la!=lb){
                printf("%d\n",ds);
                continue;
            }
            if(!la) la=lb=-INF;
            if(!ra) ra=rb=INF;
            if(ta) printf("%d\n",Abs(cs-ct)+min(Abs(rs+rt-(la<<1)),Abs(rs+rt-(ra<<1))));
            else printf("%d\n",Abs(rs-rt)+min(Abs(cs+ct-(la<<1)),Abs(cs+ct-(ra<<1))));
        }
    }
    return 0;
}

P3960 (NOIP2017 提高组)列队

题意

有一个 \(n\times m\) 的矩阵 \(a\)\(a_{i,j}=(i-1)\times m+j\)。现在有 \(q\) 组操作:

(形式化表述)将 \(v\leftarrow a_{i,j}\),然后 \(\forall k\in[j+1,m],a_{i,k-1}\leftarrow a_{i,k}\)\(\forall k\in[i+1,n],a_{i,m}\rightarrow a_{i-1,m}\),最后 \(a_{n,m}\leftarrow v\)。求出每次操作后的 \(v\)

解法

一种普通的做法是动态开点的 splay 或 FHQ Treap。

考虑操作的内容:对某一行(不包括最后一个数)的中间删去一个数,在后面增加一个数(这个操作可能不存在);对最后一列采取同样做法。

考虑在删除时,我们不用真正地删除某个数后将其余数左移,这样则需要统计原数列的某个前缀中,实际存在且保留原来的顺序的数的个数。由于开始时的数是确定的,可以使用 \(01\) 序列而非原数列维护。对于新加入的数,使用 vector 存一下,保证空间复杂度。这样需要对于某个数列,维护两个 \(01\) 序列,初始全为 \(1\),(一个对应原数组,一个对应 vector)删除时查询第 \(k\)\(1\) 的位置。显然可以用动态开点线段树维护。

听说可以把操作离线下来使用树状数组,但是我不清楚具体原理。

代码

点此查看代码
//FHQ Treap Version
#include <bits/stdc++.h>
using namespace std;
random_device rd;
mt19937 Rand(rd());
const int maxn=300010;
int n,m,q,i,xa,ya,tot;
int xt,yt,zt,xc,yc,zc;
long long ans;
struct node{
    int ls,rs,len,siz;
    long long st;
    unsigned key;
}tr[maxn*6];
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define st(p) tr[p].st
#define len(p) tr[p].len
#define siz(p) tr[p].siz
#define key(p) tr[p].key
int root[maxn];
inline int New(const long long v,const unsigned &dep,const unsigned &stp){
    ++tot;st(tot)=v;
    len(tot)=siz(tot)=1;
    key(tot)=Rand()%stp+dep;
    return tot;
}
int Build(const int l,const int r,const unsigned &dep,const unsigned &stp){
    if(l>r) return 0;
    const int mid=(l+r)>>1;
    const int rt=New(1LL*mid*m,dep,stp);
    if(l==r) return rt;
    ls(rt)=Build(l,mid-1,dep+stp,stp);
    rs(rt)=Build(mid+1,r,dep+stp,stp);
    siz(rt)=r-l+1;return rt;
}
inline void Pushup(const int &p){
    siz(p)=siz(ls(p))+siz(rs(p))+len(p);
}
int Merge(const int x,const int y){
    if(!(x&&y)) return x|y;
    if(key(x)<key(y)){
        rs(x)=Merge(rs(x),y);
        Pushup(x);return x;
    }
    else{
        ls(y)=Merge(x,ls(y));
        Pushup(y);return y;
    }
}
void sSplit(const int p,int siz,int &x,int &y,int &z){
    if(siz(ls(p))>=siz){
        y=p;
        sSplit(ls(p),siz,x,ls(p),z);
    }
    else{
        siz-=siz(ls(p));
        if(siz<=len(p)){
            z=++tot;
            st(z)=st(p)+siz-1;
            key(z)=Rand();
            len(z)=siz(z)=1;
            if(siz<len(p)){
                y=++tot;key(y)=Rand();
                st(y)=st(p)+siz;
                len(y)=siz(y)=len(p)-siz;
                y=Merge(y,rs(p));
            }
            else y=rs(p);
            const int q=ls(p);
            if(siz>1){
                x=p;key(x)=Rand();
                siz(x)=len(x)=siz-1;
                ls(x)=rs(x)=0;
                x=Merge(q,x);
            }
            else x=q;
            return;
        }
        siz-=len(p);x=p;
        sSplit(rs(p),siz,rs(p),y,z);
    }
    Pushup(p);
}
inline unsigned Log2(int x){
    int ret=1;
    if(x>>16){x>>=16;ret|=16;}
    if(x>>8){x>>=8;ret|=8;}
    if(x>>4){x>>=4;ret|=4;}
    if(x>>2){x>>=2;ret|=2;}
    if(x>>1){x>>=1;++ret;}
    return ret;
}
int main(){
    scanf("%d%d%d",&n,&m,&q);
    for(i=1;i<=n;++i){
        root[i]=++tot;
        st(tot)=(i-1LL)*m+1;
        len(tot)=siz(tot)=m-1;
        key(tot)=Rand();
    }
    root[n+1]=Build(1,n,0,4294967295u/Log2(n)); 
    while(q--){
        scanf("%d%d",&xa,&ya);
        if(ya<m){
            sSplit(root[xa],ya,xt,yt,zt);
            sSplit(root[n+1],xa,xc,yc,zc);
            ans=st(zt);
            root[xa]=Merge(Merge(xt,yt),zc);
            root[n+1]=Merge(Merge(xc,yc),zt); 
        }
        else{
            sSplit(root[n+1],xa,xt,yt,zt);
            ans=st(zt);
            root[n+1]=Merge(Merge(xt,yt),zt);
        }
        printf("%lld\n",ans);
    }
    return 0;
}
点此查看代码
//快了一点点但是好调了亿点点的线段树做法
#include <bits/stdc++.h>
using namespace std;
const int maxn=600010;
const int maxl=30;
#define ll long long
int n,m,q,i,x,y,tot;
ll t1,t2;
struct node{
    int ls,rs,sum;
}tr[maxn*maxl];
struct seg{
    int root,siz;
    ll st,gc;
    vector<ll> New;
    #define ls(p) tr[p].ls
    #define rs(p) tr[p].rs
    #define sum(p) tr[p].sum
    inline ll Query(int th){
        int p=root,lt=1,rt=siz+q,mt,sm;
        while(lt<rt){
            --sum(p);
            mt=(lt+rt)>>1;
            if(!ls(p)) sm=mt-lt+1;
            else sm=sum(ls(p));
            if(th<=sm){
                rt=mt;
                if(!ls(p)){
                    ls(p)=++tot;
                    sum(ls(p))=sm;
                }
                p=ls(p);
            }
            else{
                th-=sm;
                lt=mt+1;
                if(!rs(p)){
                    rs(p)=++tot;
                    sum(rs(p))=rt-mt;
                }
                p=rs(p);
            }
        }
        sum(p)=0;
        if(lt>siz) return New[lt-siz-1];
        return st+gc*lt;
    }
}T[maxn];
int main(){
    scanf("%d%d%d",&n,&m,&q);
    for(i=1;i<=n;++i){
        T[i].root=++tot;
        T[i].siz=m-1;
        T[i].st=(i-1LL)*m;T[i].gc=1;
        tr[tot].sum=q+m-1;
    }
    T[0].root=++tot;
    T[0].siz=n;
    T[0].gc=m;
    tr[tot].sum=q+n;
    for(i=1;i<=q;++i){
        scanf("%d%d",&x,&y);
        t2=T[0].Query(x);
        if(y<m){
            t1=T[x].Query(y);
            T[x].New.emplace_back(t2);
        }
        else t1=t2;
        T[0].New.emplace_back(t1);
        printf("%lld\n",t1);
    }
} 

P5445 (APIO2019) 路灯

题意

有一个数轴,每个点 \(i(i\in [1,n])\) 连有一条从 \(i\)\(i+1\) 的有向边。开始时有一些点不能通行。

初始时刻为 \(0\) 时刻,有 \(q\) 个操作,每个时刻有一个操作:

  • 使 \(i\) 点的通行状态改变(能通行变为不能通行,不能通行变为能通行)。
  • 询问 \(0\) 时刻开始到当前时刻,有多少个时刻能从 \(a\)\(b\)

\(n,q\le 3\cdot 10^5\)

解法

GaryH 说这个题的难度 << 原题识别,但是这个题都花了我这么长时间,在 c++ 玄学特性上几乎自闭

考虑维护一个三维数组 \(T\),其中 \(T_{u,v,t}\) 表示在 \(1\sim t\) 时刻内 \(u\rightarrow \bold{v-1}\) 连通的时刻有多少(\(v<u\) 时结果未定义)。

可以在 \(t\) 时刻维护 \(T_{1\sim n,1\sim n,t}\),把 \(T\) 压缩一维。

同时需要记 \(i\) 能够到达的编号最大的点的编号减一\(r_i\)。则在使某个点的通行状态改变时(下面记连通的极大子图为连通块),

  • \(i\) 之前不连通,则将其两边连通块内的点的 \(r\) 值记为右边连通块内的 \(r\)
  • 否则,将其左部连通的点的 \(r\) 值设为 \(i-1\),将其右部连通的点的 \(r\)设为本身

上述强调将某些 \(r\) 值设为自己将会在将来的优化中利用。

设好这些 \(r\) 值后,对于 \(\forall i\in[1,n](\text{i 处于连通状态}),v\in(i,r_i]\)\(T_{i,v}\leftarrow T_{i,v}+1\)

由于 \(T_{u,v}\)\(v<u\) 时结果未定义,故而可以对 \(\forall v\in[1,i)\)\(T_{i,v}\) 任意赋值。由于上述赋值的区域是一个三角形区域,我们可以把此区域变为矩形区域。

最后的操作本质即是矩形加,单点求和。

考虑将矩形加变为单点。通过二维前缀和可以把矩形加单点求和变成单点加矩形求和,具体可以用树状数组套动态开点线段树将单次查改时间复杂度优化成 \(O(\log^2n)\)

GaryH 使用的树状数组套使用 unordered_map 的树状数组复杂度相同,看似常数小,但是用我的写法后,即使开 O2 并且用上手写的哈希函数在洛谷上会 T 掉两个点,用自带哈希函数在洛谷上会 M 掉一个点(又或者是 GaryH 自带小常数?)

但是每次操作后单点加的次数会是 \(O(n)\) 的,单次询问/修改操作的时间复杂度高达 \(O(n\log^2n)\)

考虑优化。记 \(t_i\) 表示 \(\forall j,r_j=i\) 组成的连通块上一次形成/改变的时刻,则在每次操作后最多涉及两个 \(t\) 值的连通块,可以在连通块合并/分裂时再对矩形区域(单点区域)加上目前时刻减去上一次的 \(t\) 值(类似于懒标记)。这样则单次询问/修改操作的时间复杂度为 \(O(\log^2n)\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=300010;
const int maxl=100;
int n,q,u,v,i,j,lt,rt,mt,pt,tot;
int r[maxn];
char x[maxn];
set<int> s;
set<int>::iterator it,i1,i2;
struct node{
    int ls,rs,sum;
}tr[maxn*maxl];
struct seg{
    #define ls(p) tr[p].ls
    #define rs(p) tr[p].rs
    #define sum(p) tr[p].sum
    int root;
    inline void add(const int &p,const int &d){
        lt=1;rt=n;pt=root;
        while(lt<rt){
            mt=(lt+rt)>>1;
            sum(pt)+=d;
            if(p<=mt){
                if(ls(pt)) pt=ls(pt);
                else{
                    ls(pt)=++tot;
                    pt=tot;
                }
                rt=mt;
            }
            else{
                if(rs(pt)) pt=rs(pt);
                else{
                    rs(pt)=++tot;
                    pt=tot;
                }
                lt=mt+1;
            }
        }
        sum(pt)+=d;
    }
    int query(const int &rd){
        lt=1;rt=n;pt=root;ret=0;
        for(;;){
            if(rt<=rd) return sum[pt]+ret;
            mt=(lt+rt)>>1;
            if(mt<rd){
                ret+=sum[ls[pt]];
                lt=mt+1;pt=rs[pt];
            }
            else{
                rt=mt;
                pt=ls[pt];
            }
        }
    } 
};
struct bit2{
    seg c[maxn];
    inline void add(int p,const int &y,const int &d){
        for(;p<=n;p+=(p&-p)) c[p].add(y,d); 
    }
    inline void rect(const int &cs,const int &ct,const int &d){
        add(cs,cs,d);
        add(cs,ct,-d);
        add(ct,cs,-d);
        add(ct,ct,d);
    }
    inline int query(int p,const int &y){
        int ret=0;
        for(;p;p^=(p&-p)) ret+=c[p].query(y);
        return ret;
    }
}M;
int main(){
    scanf("%d%d%s",&n,&q,x+1);
    for(i=1;i<=n;++i){
        if(x[i]=='0') s.insert(i);
        else r[i]=-1;
        M.c[i].root=i;
    }
    M.c[n+1].root=tot=n+1;
    s.insert(++n);s.insert(0);
    for(j=1;j<=q;++j){
        scanf("%s%d",x,&i);
        if(x[0]=='t'){
            if(~r[i]){
                i1=i2=it=s.find(i);
                u=*(--i1)+1;v=*(++i2)-1;
                M.rect(u,i,j-r[i]);
                M.rect(i+1,v+1,j-r[v+1]);
                r[v+1]=j;r[i]=-1;s.erase(it);
            }
            else{
                i1=it=s.lower_bound(i);v=*it;
                M.rect(*(--i1)+1,v,j-r[v]);
                r[v]=r[i]=j;s.insert(i);
            }
        }
        else{
            scanf("%d",&v);
            u=M.query(i,--v);
            if(!((~r[i])||(~r[v]))){
                it=s.upper_bound(i);
                if(*it>v) u+=j-r[*it]; 
            }
            printf("%d\n",u);
        }
    }
    return 0;
}

P7949 左方之地 &(ARC138D)

题意

要求构造一个 \(0\sim 2^n-1\) 的排列 \(P\),满足相邻两个数的异或值在二进制的表示下刚好有且只有 \(k\)\(1\)。求任意一种构造方案,或判断其为无解。ARC138D:需要最后一个数和第一个数的异或值满足同样的性质。

P7949:\(n,k\le 20\)。ARC138D:\(k\le n\le 18\)

解法

首先 \(k\) 为偶数时(考虑奇偶性)或不小于 \(n\) 时显然无解。

考虑格雷码(\(k=1\) 的情况)。长为 \(2^n\) 的格雷码 \(S\)\(O(2^n)\) 的构造方式:第 \(i\) 项为 \(i\oplus\lfloor\frac i2\rfloor\)证明

所以我们有了对于任意的 \(n\) 构造 \(k=1\) 的解法。同时考虑构造 \(k=n-1\) 的方案。发现如果把奇数位的 \(S\) 值的所有位取反,则对于取反的 \(S_i\)\((S_{i-1}\oplus S_i)_2\)\((S_{i+1}\oplus S_i)_2\) 均有 \(n-1\)\(1\)。同时 \((S_{2^n-1}\oplus S_0)_2\) 有同样的性质。

考虑从 \(n=k+i\) 的合法解推到 \(n=k+i+1\) 的合法解。可以构造 \(S_{2^{k+i}-1+j}=S_{2^{k+i}-j}\oplus(2^{k-1}-1)\oplus2^{k+i}\),此时 \((S_{2^{k+i}}\oplus S_{2^{k+i}-1})_2\)\(k\)\(1\),而 \(S_{2^{k+i}+p-1}\oplus S_{2^{k+i}+p}=(S_{2^{k+i}-p}\oplus ((2^{k-1}-1)\oplus 2^{k+i}))\oplus (S_{2^{k+i}-p-1}\oplus((2^{k-1}-1)\oplus2^{k+i})=S_{2^{k+i}-p}\oplus S_{2^{k+i}-p-1}\),同样在二进制表达下有 \(k\)\(1\)。新的 \(S_{2^n-1}\oplus S_0\) 同理。同时若 \(\exists a,b\ge 2^{k+i}\),则 \(S_a=S_b\)\(S_{2^{k+i+1}-1-a}\oplus((2^{k-1}-1)\oplus 2^{k+i})=S_{2^{k+i+1}-1-b}\oplus((2^{k-1}-1)\oplus 2^{k+i})\),也就是需要 \(S_{2^{k+i+1}-1-a}=S_{2^{k+i+1}-1-b}\),而此条件满足当且仅当 \(a=b\)。综上,这样的构造方法可以得到 \(n=k+i+1\) 的合法解。

综上,可以先求 \(n=k+1\) 的合法解,然后逐级递推,即可求得答案。

代码

点此查看代码
//P7949
#include <bits/stdc++.h>
using namespace std;
int n,k,s,i,j,l,q;
int b[1048600];
int main(){
    scanf("%d%d",&n,&k);
    q=k;
    if(n==1&&k==1){
        printf("1\n0 1");
        return 0;
    }
    if(k>=n||(!(k&1))){
        printf("0\n");
        return 0;
    }
    printf("1\n");
    s=(1<<(k+1))-1;
    for(i=0;i<=s;++i){
        b[i]=i^(i>>1);
        if(i&1) b[i]^=s;
    }
    l=s;s=(1<<(k-1))-1;++k;
    while(k<n){
        j=l+1;
        for(i=l;i>=0;--i) b[j++]=(b[i]+(1<<k))^s;
        l=l<<1|1;++k;
    }
    for(i=0;i<=l;++i) printf("%d ",b[i]);
    return 0;
}
posted @ 2022-10-04 15:59  Fran-Cen  阅读(168)  评论(0编辑  收藏  举报