三月好题

Black and White Rooks

题意

有一个 \(n\times m\) 的棋盘。现在需要在上面放置 \(b\) 个黑车和 \(w\) 个白车,每个车占有一格且不能有共行或共列的黑车和白车(不存在一对能互相攻击的车)。求放法数量模 \(998244353\)\(n,m\le 50\)

解法

考虑用 \(x\) 个同色的车攻击到 \(i\)\(j\) 列的方案数。首先随机放置的方案数显然为 \(\binom{ij}{x}\)。而其中需要减掉只攻击到 \(i'\)\(j'\) 列的方案数。设 \(dp_{i,j,x}\) 表示用 \(x\) 个同色的车攻击到 \(i\)\(j\) 列的方案数,则转移有 \(dp_{i,j,x}=\binom{ij}{x}-\sum_{i'=1}^i\sum_{j'=1}^jdp_{i',j',x}\binom{i}{i'}\binom{j}{j'}\)。最后答案即为 \(\sum_{i=1}^n\sum_{j=1}^m\sum_{k=1}^{n-i}\sum_{p=1}^{m-j}dp_{i,j,b}dp_{k,p,w}\binom{n}{i}\binom{m}{j}\binom{n-i}{k}\binom{m-j}{p}\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2510;
const int md=998244353;
#define ll long long
int n,m,i,j,k,p,b,w;
ll d,v,d1,d2,fac[maxn],inv[maxn];
ll B[55][55],W[55][55];
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;
}
inline ll C(const int &x,const int &y){
    if(x>y) return 0;
    ll ret=fac[y]*inv[x];
    if(ret>=md) ret%=md;
    ret*=inv[y-x];
    if(ret>=md) ret%=md;
    return ret;
}
int main(){
    scanf("%d%d%d%d",&n,&m,&b,&w); 
    fac[0]=fac[1]=inv[0]=1;
    for(i=2;i<=maxn;++i){
        fac[i]=fac[i-1]*i;
        if(fac[i]>=md) fac[i]%=md;
    }
    inv[maxn]=Pow(fac[maxn],md-2);
    for(i=maxn-1;i;--i){
        inv[i]=inv[i+1]*(i+1);
        if(inv[i]>=md) inv[i]%=md;
    }
    for(i=1;i<=n;++i){
        for(j=1;j<=m;++j){
            v=C(b,i*j);
            if(b<i*j){
                for(k=1;k<=i;++k){
                    d1=C(k,i);
                    for(p=1;p<=j;++p){
                        d2=C(p,j);
                        d=B[k][p]*d1;if(d>=md) d%=md;
                        d*=d2;if(d>=md) d%=md;
                        v-=d;if(v<0) v+=md;
                    }
                }
            }
            B[i][j]=v;
            v=C(w,i*j);
            if(w<i*j){
                for(k=1;k<=i;++k){
                    d1=C(k,i);
                    for(p=1;p<=j;++p){
                        d2=C(p,j);
                        d=W[k][p]*d1;if(d>=md) d%=md;
                        d*=d2;if(d>=md) d%=md;
                        v-=d;if(v<0) v+=md;
                    }
                }
            }
            W[i][j]=v;
        }
    }
    d=0;
    for(i=1;i<n;++i){
        for(j=1;j+i<=n;++j){
            for(k=1;k<m;++k){
                for(p=1;p+k<=m;++p){
                    d1=B[i][k]*C(i,n);if(d1>=md) d1%=md;
                    d2=W[j][p]*C(j,n-i);if(d2>=md) d2%=md;
                    d1*=C(k,m);if(d1>=md) d1%=md;
                    d2*=C(p,m-k);if(d2>=md) d2%=md;
                    d1*=d2;if(d1>=md) d1%=md;d+=d1;if(d>=md) d-=md;
                }
            }
        }
    }
    printf("%lld",d);
    return 0;
}

Range Pairing Query

题意

有一个长为 \(n\) 的数组 \(a\)。现在进行 \(q\) 次询问,每次询问给出一个区间 \([l,r]\),求 \(a_l\sim a_r\) 中最多可以组成多少数值相同的数对。\(n\le 10^5,q\le 10^6\)

解法

模板莫队题(不过似乎没有好一点的非莫队写法)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
const int maxq=1000010;
int n,q,i,l,r,tl,tr; 
int val[maxn],app[maxn];
struct Query{
    int l,r,lb,idx;
    inline bool operator <(const Query &a)const{
        if(lb!=a.lb) return lb<a.lb;
        if(r==a.r) return 0;
        return (lb>a.lb)^(r<a.r);
    }
}Q[maxq],*pt; 
long long a[maxq],ans;
int main(){
    scanf("%d",&n);const int siz=sqrt(n);
    for(i=1;i<=n;++i) scanf("%d",val+i);
    scanf("%d",&q);pt=Q+1;
    for(i=1;i<=q;++i){
        scanf("%d%d",&(pt->l),&(pt->r));
        pt->lb=pt->l/siz;pt->idx=i;++pt;
    }
    sort(Q+1,Q+q+1);
    pt=Q+1;
    r=(l=pt->l)-1;
    for(i=1;i<=q;++i){
        tl=pt->l;tr=pt->r;
        while(l>tl) if((app[val[--l]]++)&1) ++ans;
        while(r<tr) if((app[val[++r]]++)&1) ++ans;
        while(l<tl) if((--app[val[l++]])&1) --ans;
        while(r>tr) if((--app[val[r--]])&1) --ans;
        a[pt->idx]=ans;++pt;
    }
    for(i=1;i<=q;++i) printf("%lld\n",a[i]);
    return 0;
}

Power Board

题意

有一个 \(n\times m\) 的矩阵 \(a\),满足 \(a_{i,j}=i^j\)。求 \(a\) 中有多少不同的数值。\(n,m\le 10^6\)

解法

显然不论是数本身还是某些数在其他地方的出现都难以维护,此时考虑每一行有多少书在之后的几行中出现过,防止重复。

考虑 \(p^{k_1}\) 行和 \(p^{k_2}\) 行(\(p\ne 1\))的相同的数的个数。可得这两行相同的数为 \(p^{s\text{lcm}(k_1,k_2)}(s\le \frac{\min(k_1,k_2)m}{\text{lcm}(k_1,k_2)})\),从而可以枚举 \(k_1,k_2\),依次算出其相同的数的个数。由于 \(k_1,k_2<20\)\(2^{20}>n\)),所以复杂度可以接受,为 \(O(n\log^2n)\)

我们同时要确定 \(2\sim n\)\(\forall i\) 若写成 \(p^k\) 的形式(\(p,k\in N_+\))的最大 \(k\)。使用欧拉筛+对 \(1\sim 20\) 的两两 \(\gcd\) 打表,复杂度同样可以保证。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1000010;
const int maxt=78600;
int n,m,i,x,b,j,k,t,pw,cnt,top;
int v[maxn],p[maxt],s[21],a[21],sam[21][21];
long long ans;
const int gcd[21][21]={{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20},
                       {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
                       {2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2,1,2},
                       {3,1,1,3,1,1,3,1,1,3,1,1,3,1,1,3,1,1,3,1,1},
                       {4,1,2,1,4,1,2,1,4,1,2,1,4,1,2,1,4,1,2,1,4},
                       {5,1,1,1,1,5,1,1,1,1,5,1,1,1,1,5,1,1,1,1,5},
                       {6,1,2,3,2,1,6,1,2,3,2,1,6,1,2,3,2,1,6,1,2},
                       {7,1,1,1,1,1,1,7,1,1,1,1,1,1,7,1,1,1,1,1,1},
                       {8,1,2,1,4,1,2,1,8,1,2,1,4,1,2,1,8,1,2,1,4},
                       {9,1,1,3,1,1,3,1,1,9,1,1,3,1,1,3,1,1,9,1,1},
                       {10,1,2,1,2,5,2,1,2,1,10,1,2,1,2,5,2,1,2,1,10},
                       {11,1,1,1,1,1,1,1,1,1,1,11,1,1,1,1,1,1,1,1,1},
                       {12,1,2,3,4,1,6,1,4,3,2,1,12,1,2,3,4,1,6,1,4},
                       {13,1,1,1,1,1,1,1,1,1,1,1,1,13,1,1,1,1,1,1,1},
                       {14,1,2,1,2,1,2,7,2,1,2,1,2,1,14,1,2,1,2,1,2},
                       {15,1,1,3,1,5,3,1,1,3,5,1,3,1,1,15,1,1,3,1,5},
                       {16,1,2,1,4,1,2,1,8,1,2,1,4,1,2,1,16,1,2,1,4},
                       {17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,17,1,1,1},
                       {18,1,2,3,2,1,6,1,2,9,2,1,6,1,2,3,2,1,18,1,2},
                       {19,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,19,1},
                       {20,1,2,1,4,5,2,1,4,1,10,1,4,1,2,5,4,1,2,1,20}};
int main(){
    scanf("%d%d",&n,&m);
    for(j=1;j<20;++j){
        for(k=j+1;k<=20;++k){
            b=k/gcd[j][k];
            for(i=b;i<=m;i+=b){
                if(!v[i]){
                    v[i]=1;
                    ++cnt;
                }
            }
            sam[j][k]=cnt;
        }
        cnt=0;
        for(i=2;i<=m;++i) v[i]=0;
    }
    ans=(n-1ll)*m+1;
    for(i=2;i<=n;++i){
        if(!v[i]) v[i]=p[++top]=i;
        for(j=1;j<=top;++j){
            if(i*p[j]>n||p[j]>v[i]) break;
            v[i*p[j]]=p[j];
        }
        x=i;cnt=pw=0;
        while(x!=1){
            b=v[x];
            while(v[x]==b){
                x/=b;
                ++cnt;
            } 
            if(pw!=1) pw=gcd[cnt][pw];
            s[++t]=cnt;a[t]=b;cnt=0;
        }
        cnt=1;
        for(j=1;j<=t;++j)
            for(k=pw;k<=s[j];k+=pw)
                cnt*=a[j];
        x=cnt;b=n/cnt;j=1;t=0;
        while(x<=b){x*=cnt;++j;}
        ans-=sam[pw][j];
    }
    printf("%lld\n",ans);
    return 0;
}

Tyler ans Strings

题意

给定一个长为 \(n\) 的字符串 \(s\) 和一个长为 \(m\) 的字符串 \(t\),求将 \(s\) 重排可得的字典序小于 \(t\) 的字符串总数模 \(998244353\)\(n,m,s_i,t_i\le 2\cdot 10^5\)

解法

由于字典序的比较大小是从前到后进行比较的,两个字符串前面某个数不一样就不考虑后面,故而在试填 \(s\) 重排的字符串也可以从前往后讨论。

我们维护一个指针 \(p\) 表示目前填到了第 \(p\) 位。此时仍然能填的后缀长度为 \(|s|-p\)(不考虑第 \(p\) 位)。令第 \(p\) 位小于 \(t_p\) 的字符中字符 \(i\)\(c_i\) 个,则之后的字符串后缀的种类为

\[\sum_{i=1}^{t_p-1}[c_i]\frac{(n-p)!}{\prod_{j=1}(c_j-[j=i])!}=\sum_{i=1}^{t_p-1}c_i\frac{(n-p)!}{\prod_{j=1}c_j!} \]

可以用树状数组维护 \(\sum_{i=1}^{t_p-1}c_i\)。考虑从 \(\frac{(n-p)!}{\prod_{j=1}c_j!}\) 推到 \(\frac{(n-p-1)!}{\prod_{j=1}c_j!}\),发现只需要乘上一个 \(\frac{c_{t_p}}{n-p}\) 即可。可以线性递推。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define Inv(a) Pow(a,md-2)
const int maxn=200010;
const ll md=998244353;
int n,m,i,a,b;
bool f;
long long s,ans,cnt[maxn],fac[maxn],inv[maxn],invf[maxn];
inline ll Pow(ll d,ll 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;
}
struct BIT{
    int c[maxn];
    inline void Add(int p,const long long &v){for(;p<maxn;p+=(p&-p)) c[p]+=v;}
    inline void Sub(int p){for(;p<maxn;p+=(p&-p)) --c[p];}
    inline int Sum(int p){
        int ret=0;
        for(;p;p^=(p&(-p))) ret+=c[p];
        return ret;
    }
}C;
int main(){
    fac[1]=inv[1]=invf[1]=1;
    for(i=2;i<maxn;++i){
        fac[i]=fac[i-1]*i;
        inv[i]=Pow((long long)i,md-2);
        invf[i]=invf[i-1]*inv[i];
        if(fac[i]>=md) fac[i]%=md;
        if(invf[i]>=md) invf[i]%=md;
    }
    scanf("%d%d",&n,&m);
    if(n<m) f=1;
    for(i=1;i<=n;++i){
        scanf("%d",&a);
        ++cnt[a];
    }
    s=fac[n-1];
    for(i=1;i<maxn;++i){
        if(!cnt[i]) continue;
        C.Add(i,cnt[i]);
        s*=invf[cnt[i]];
        if(s>=md) s%=md;
    }
    for(i=1;i<=m;++i){
        if(!n) break;
        scanf("%d",&b);
        ans+=s*C.Sum(b-1);
        if(ans>=md) ans%=md;
        if(!cnt[b]) break;
        s*=cnt[b];
        if(s>=md) s%=md;
        s*=inv[n-1];
        if(s>=md) s%=md;
        --n;--cnt[b];
        C.Sub(b);
    }
    if(f&&(!n)) ++ans;
    if(ans==md) ans=0;
    printf("%lld",ans);
}

Serious Business

题意

有一个 \(3\times n\) 的矩阵 \(a\),其中有且只有第二行不可通行。

现在有 \(q\) 种可选择的操作,第 \(i\) 种操作可以使第二行 \([l_i,r_i]\) 可以通行,花费 \(k_i\)

选择好操作后,需要从第一行第一列每次向下或向右走一格直到走到第三行第 \(n\) 列,收益为走到的所有数之和。

求最大净收益。\(n,q\le 5\cdot 10^5,-10^9\le a_{i,j}\le 10^9\)

解法

考虑拆分某种走法的贡献。

设第二行中经过了 \([l,r]\) 的区间,则可得总收益为 \(\sum_{i=1}^la_{1,i}+\sum_{i=1}^{l-1}a_{2,i}+\sum_{i=1}^ra_{2,i}+\sum_{i=1}^{r-1}a_{3,i}+\sum_{i=1}^na_{3,i}\)

\(d_l\) 表示 \(\sum_{i=1}^la_{1,i}+\sum_{i=1}^{l-1}a_{2,i}\)\(f_r\) 表示 \(\sum_{i=1}^ra_{2,i}+\sum_{i=1}^{r-1}a_{3,i}+\sum_{i=1}^na_{3,i}\),则总收益为 \(d_l+f_r\)

考虑区间是否允许通行,设 \(d'_i\) 表示恰好走到第二行第 \(i\)的最大收益,(显然 \(d'_i\) 初值为 \(d_i\))若有一个区间为 \([l,r]\) 且解锁代价为 \(k\),则有

\[d'_{i}(l\le i\le r)=\max(d'_i,\max_{l\le j\le r}d'_{j}-k) \]

我们理论上要将所有 \(i\) 均进行上述处理,但是若将所有区间按右端点升序排序,则在 \(d'_r\) 处更新即可以代替上述其他的 \(i\),且 \(\max_{l\le j\le r}d'_{j}-k\) 关于 \(i\) 单调不降,故而只更新 \(d'_r\) 一定更优。在更新时可以顺便统计答案 \(\max_{l\le j\le p\le r}d'_{j}+f_p-k\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=500010;
int n,q,i,j,la,ra,ka;
long long a,sm,dm,f1[maxn],f2[maxn],pre[3][maxn];
struct len{
    int lt,rt,kt;
    inline bool operator <(const len &a)const{return rt<a.rt;}
}N[maxn];
int l[maxn<<2],r[maxn<<2],m[maxn<<2];
long long d[maxn<<2],f[maxn<<2],s[maxn<<2];
#define ls(p) p<<1
#define rs(p) p<<1|1
inline void Pushup(const int &p){
    s[p]=max(max(s[ls(p)],s[rs(p)]),d[ls(p)]+f[rs(p)]);
    d[p]=max(d[ls(p)],d[rs(p)]);
    f[p]=max(f[ls(p)],f[rs(p)]);
}
void build(const int p,const int lx,const int &rx){
    l[p]=lx;
    r[p]=rx;
    m[p]=(lx+rx)>>1;
    if(lx==rx){
        d[p]=f1[lx];
        f[p]=f2[lx];
        s[p]=f1[lx]+f2[lx];
        return;
    }
    build(ls(p),lx,m[p]);
    build(rs(p),m[p]+1,rx);
    Pushup(p);
}
void change(const int p,const int &x,const long long &v){
    if(l[p]==r[p]){
        if(d[p]<v){
            d[p]=v;
            s[p]=v+f[p];
        }
        return;
    }
    if(x<=m[p]) change(ls(p),x,v);
    else change(rs(p),x,v);
    Pushup(p);
}
void query(const int p,const int &lt,const int &rt){
    if(lt<=l[p]&&rt>=r[p]){
        sm=max(sm,max(s[p],dm+f[p]));
        dm=max(dm,d[p]);
        return;
    }
    if(lt<=m[p]) query(ls(p),lt,rt);
    if(rt>m[p]) query(rs(p),lt,rt);
} 
int main(){
    scanf("%d%d",&n,&q);
    for(i=0;i<3;++i){
        for(j=1;j<=n;++j){
            scanf("%lld",&a);
            pre[i][j]=pre[i][j-1]+a;
        }
    }
    a=pre[2][n];
    for(i=1;i<=n;++i){
        f1[i]=pre[0][i]-pre[1][i-1];
        f2[i]=pre[1][i]-pre[2][i-1]+a;
    }
    build(1,1,n);
    for(i=1;i<=q;++i) scanf("%d%d%d",&((N+i)->lt),&((N+i)->rt),&((N+i)->kt));
    sort(N+1,N+q+1);
    a=-9e18;
    for(i=1;i<=q;++i){
        sm=dm=-9e18;
        la=N[i].lt;
        ra=N[i].rt;
        query(1,la,ra);
        a=max(a,sm-N[i].kt);
        if(ra!=n) change(1,ra+1,dm-N[i].kt);
    }
    printf("%lld",a);
    return 0;
}

Circular Addition

题意

有一个长为 \(n\) 的环状数组 \(x\),初始全为 \(0\)。可以进行若干次操作,每次操作把 \(x\) 上一段连续段加上 \(1\)。求把 \(x\) 变成某个给定的环状数组 \(a\) 的最小操作次数。\(n\le 2\cdot 10^5,1\le a_i\le 10^9\)

解法

\(x\) 非环状,则可以处理差分数组 \(d\),然后 \(\sum_{i=1}^{\bold{n+1}}[d_i>0]d_i\) 即为答案。

问题中的环状数组同样可以考虑成线性数组,但是 \([i,n]\)\([1,j](j<i)\) 的区间加只算一次。这样操作后,\(d_i\)\(d_1\) 会多加一次,而 \(d_{n+1}\)\(d_{j+1}\) 会多减一次。将 \(d_1\)\(d_{n+1}\) 的加一次和减一次抵消掉直到无法再抵消即可。不过操作次数有一个下限为 \(\max_{i=1}^na_i\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
int n,i,j,t,x;
int a[maxn],b[maxn];
long long d;
int main(){
    scanf("%d",&n);
    for(i=1;i<=n;++i){
        scanf("%d",a+i);
        if(a[i]>x) x=a[i];
    }
    for(i=2;i<=n;++i) if(a[i]>a[i-1]) d+=a[i]-a[i-1];
    if(a[1]>a[n]) d+=a[1]-a[n];
    if(d<x) d=x;
    printf("%lld",d);
}

Non-coprime DAG

题意

有一张有 \(n\) 个点的有向图,第 \(i\) 个点有点权 \(a_i\)。若 \(\exists i,j,i<j,\gcd(i,j)>1\),则存在从 \(i\)\(j\) 的有向边。现在要求选出一些点,满足这些点两两不可达。求选中的点的最大 \(a\) 值之和。\(n\le 2\cdot 10^5,1\le a_i\le 10^9\)

解法

听说这个题理解很简单但是实战会极其难想

考虑能从哪些小于 \(i\) 的点 \(j\)\(i\)

  • \(j\)\(i\) 均为偶数,则显然 \(j\) 能到 \(i\)

\[p_i=\begin{cases}\inf,& i=1,\\\min_{j=2}^i[i\!\!\mod j\ne 0]\inf+j,& i\!\!\mod 2=1\ \cap\ i>1,\\0, &i\!\!\mod 2=0\ \cap\ i>1.\end{cases} \]

  • \(j\) 为奇数,\(i\) 为偶数,则 \(j\) 可以到的最小的偶数点为 \(j+p_j\),此时需要 \(j+p_j\le i\)
  • \(i\) 为奇数,\(j\) 为偶数,则能到达 \(i\) 的最小的偶数点为 \(i-p_i\),此时需要 \(j\le i-p_i\)
  • \(i\)\(j\) 均为奇数,则由上述结论可知 \(j+p_j\le i-p_i\)

综上,\(j\) 能到达 \(i\)\(i\) 能到达 \(j\) 的充要条件为 \([i-p_i,i+p_i]\cap[j-p_j,j+p_j]=\emptyset\)

可以把问题抽象成有 \(n\) 个区间,第 \(i\) 个区间为 \([i-p_i,i+p_i]\),有权值 \(a_i\),需要选出若干个两两相交的区间满足权值和最大。由于不考虑 \(1\) 区间的话,所有区间均在 \([0,2n]\) 内,所以可以直接统计包括某个数 \(i\) 的区间 \(a\) 值之和 \(s_i\)\(\max s_i+a_1\) 即为答案。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1000010;
const int maxt=78600;
int n,i,t,j,a[maxn],v[maxn],p[maxt];
long long mx,c[maxn<<1];
int main(){
    scanf("%d",&n);
    for(i=1;i<=n;++i) scanf("%d",a+i);
    for(i=2;i<=n;++i){
        if(!v[i]) v[i]=p[++t]=i;
        for(j=1;j<=t;++j){
            if(i*p[j]>n||v[i]<p[j]) break;
            v[i*p[j]]=p[j];
        }
    }
    for(i=2;i<=n;i+=2) v[i]=1;
    for(i=2;i<=n;++i){
        c[i-v[i]+1]+=a[i];
        c[i+v[i]]-=a[i];
    }
    n<<=1;
    for(i=1;i<=n;++i){
        c[i]+=c[i-1];
        if(c[i]>mx) mx=c[i];
    }
    printf("%lld",mx+a[1]);
    return 0;
}

Sum of Matchings

题意

有一张有 \(2n\) 个点的二分图,左部点编号在 \(1\sim n\) 之间,右部点编号在 \(n+1\sim 2n\) 之间,每个点的度恰为 \(2\)。定义 \(f(l,r,L,R)\) 为取编号在 \(l\sim r\)\(L\sim R\) 的点组成的子图的最大匹配数。求 \(\sum_{l=1}^n\sum_{r=l}^n\sum_{L=n+1}^{2n}\sum_{R=L}^{2n}f(l,r,L,R)\)\(n\le 1500\)

解法

可以发现此图一定是由且只由若干个简单环或连通的一对点(为了简便,以下统称为环)构成。由于每个连通块在二分图匹配中互不影响,考虑单独的每个环对所有匹配之和的贡献。

  • 若某个子图完整地包含了这个环,则贡献为 \(\frac12\ \footnotesize\texttt{环长}\)
  • 若某个子图包含了这个环的一条链且包含这条链的链不在子图中(否则会算重),则贡献为 \(\lceil\frac12\ \footnotesize\texttt{链长}\rceil\)

由于环长长度为 \(n\),使用环状链表暴力统计每个环和其所有的链即可。特判会比较多。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=3010;
int n,r,i,j,u,v,t,ln,rn,lx,rx,r1,r2,cnt,len;
int h[maxn],st[maxn],nxt[maxn],pre[maxn];
long long ans;
struct edge{int to,nxt;}E[maxn<<1];
inline void Upd(const int &x){
    if(x<=r){
        if(x<ln) ln=x;
        if(x>lx) lx=x;
    }
    else{
        if(x<rn) rn=x;
        if(x>rx) rx=x;
    }
}
#define In(a,lc,rc) (a<=(rc)&&a>=(lc))
#define Sl0 (ln*(r-lx+1))
#define Sr0 ((rn-r)*(n-rx+1))
inline long long Sl1(const int &x){
    if(In(x,1,ln-1)) return (ln-x)*(r-lx+1);
    else return ln*(x-lx);
}
inline long long Sr1(const int &x){
    if(In(x,r,rn-1)) return (rn-x)*(n-rx+1);
    else return (rn-r)*(x-rx);
}
inline long long Sl2(const int &x,const int &y){
    if(In(x,1,ln-1)&&In(y,1,ln-1)) return Sl1(y);
    if(In(x,lx+1,r)&&In(y,lx+1,r)) return Sl1(x);
    return (ln-x)*(y-lx);
}
inline long long Sr2(const int &x,const int &y){
    if(In(x,r+1,rn-1)&&In(y,r+1,rn-1)) return Sr1(y);
    if(In(x,rx+1,n)&&In(y,rx+1,n)) return Sr1(x);
    return (rn-x)*(y-rx);
}
inline long long Solve1(const int &x){
    if(In(x,ln,lx)||In(x,rn,rx)) return 0;
    if(x<=r) return Sl1(x)*Sr0;
    else return Sr1(x)*Sl0;
}
inline long long Solve2(int x,int y){
    if(In(x,ln,lx)||In(x,rn,rx)) return 0;
    if(In(y,ln,lx)||In(y,rn,rx)) return 0;
    if(x>y) swap(x,y);
    if(x<=r&&y<=r) return Sl2(x,y)*Sr0;
    if(x>r&&y>r) return Sr2(x,y)*Sl0;
    return Sl1(x)*Sr1(y);
}
int main(){
    scanf("%d",&n);
    r=n;n<<=1;
    for(i=1;i<=n;++i){
        scanf("%d%d",&u,&v);
        E[++t]={u,h[v]};h[v]=t;
        E[++t]={v,h[u]};h[u]=t;
    }
    for(i=1;i<n;++i){
        if(!nxt[i]){
            ln=rn=1145141919;
            lx=rx=0;
            st[++cnt]=i;
            u=i;v=E[h[i]].to;
            Upd(u);len=1;
            while(v!=i){
                Upd(v);nxt[u]=v;j=h[v];
                if(E[j].to==u) j=E[j].nxt;
                u=v;v=E[j].to;++len;
            }
            nxt[u]=i;
            ans+=(long long)Sl0*Sr0*(len>>1);
        }
    }
    for(i=1;i<=n;++i) pre[nxt[i]]=i;
    for(i=1;i<=cnt;++i){
        u=st[i];
        for(;;){
            ln=rn=1145141919;
            lx=rx=0;len=1;
            Upd(u);r1=pre[u];
            for(v=nxt[u];nxt[v]!=u;v=r2){
                ++len;Upd(v);r2=nxt[v];
                if(r1==r2) ans+=Solve1(r1)*(len>>1);
                else ans+=Solve2(r1,r2)*(len>>1);
            }
            u=nxt[u];
            if(u==st[i]) break;
        }
    }
    printf("%lld",ans);
    return 0;
}

Tower Defense

有一个数轴,在点 \(1\sim n\) 中,点 \(i\) 有一个血量上限为 \(c_i\),回复血量速度为每秒 \(r_i\) 的初始满血的塔。在原点处有 \(q\) 只怪物,第 \(i\) 只怪物在 \(t_i\) 时开始向正方向移动,血量为 \(h_i\) 且不会回血。每只怪物移动速度均为每秒 \(1\) 单位。当目前有 \(H\) 点血量的怪物经过目前有 \(M\) 点血量的塔时,此怪物和塔均会减少 \(\min(H,M)\) 的血量。求最后所有怪物经过所有塔后的血量之和。\(n,q,t_i\le 2\cdot 10^5,r_i,c_i\le 10^9,h_i\le 10^12\)

解法

考虑点 \(i\) 的塔被第 \(j\) 和第 \(k\) 的怪物先后经过的时间差,显然为 \((t_k+i)-(t_j+i)=t_k-t_j\)

故而可以把每只怪物移动速度看作无穷大,瞬间经过可以经过的每一座塔。

考虑某个怪物对所有塔的影响,可以发现其会将一段连续的塔的血量变为 \(0\),可能会给最后一座塔留下一些血量,而不会对再后面的塔造成影响。

可以把血量为 \(0\) 的塔用一段区间维护,将血量不为 \(0\) 的塔用单点维护。单点的塔可以直接暴力计算,而涉及多个塔的区间 \([l,r]\) 的从上一次集体血量变为 \(0\) 的时刻 \(i\) 到现在的时刻 \(t\) 的血量和有如下式子:

\[\begin{aligned}B&=\sum_{j=l}^r [r_j(t-i)>c_j]c_j+\sum_{j=l}^r[r_j(t-i)\le c_j]r_j(t-i)\\&=\sum_{j=l}^r([t-i>\lfloor\frac{c_j}{r_j}\rfloor]c_j)+(t-i)\sum_{j=l}^r[t-i\le \lfloor\frac{c_j}{r_j}\rfloor]r_j\end{aligned} \]

这个问题可以看作是一个二维偏序上的问题,可以用主席树解决,但是要将所有塔按照 \(\lfloor\frac{c_j}{r_j}\rfloor\) 进行排序去重,离散化后依次插入主席树中,对 \(t-i>\lfloor\frac{c_j}{r_j}\rfloor\)\(t-i\le\lfloor\frac{c_j}{r_j}\rfloor\),在主席树上二分,可以在单次 \(O(\log n)\) 的时间内进行查询。(如果不在主席树上二分会是单次 \(O(\log^2n)\) 的)

由于每个怪物只会增加上述的一个连续区间,且在减少 \(k\) 个连续区间时,统计的总时间复杂度为 \(O(k\log n)\) 的,故而最终复杂度为 \(O(q\log n)\)

具体实现的各种细节见代码。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
const int maxd=4000010;
int n,i,q,t,de,pt,rb,lc,rc,top;
int R[maxn],L[maxn],C[maxn];
long long ans,rem;
struct tower{
    int re,ca,li,idx;
    inline bool operator <(const tower &a)const{return li<a.li;}
}N[maxn];
struct len{
    int l,r,le,ti;
    long long ma;
}S[maxn],now;
int tim,tot;
int val[maxn],rt[maxn];
struct node{
    int ls,rs;
    long long s1,s2;
}tr[maxd];
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define s1(p) tr[p].s1
#define s2(p) tr[p].s2
inline void Pushup(const int &p){
    s1(p)=s1(ls(p))+s1(rs(p));
    s2(p)=s2(ls(p))+s2(rs(p));
}
inline void Insert(const int &x){
    int p=rt[tim],lst=tot;
    rt[++tim]=tot+1;
    tr[++tot]=tr[rt[tim-1]];
    val[tim]=L[x];
    int l=1,r=n,mid;
    while(l<r){
        mid=(l+r)>>1;
        if(x<=mid){
            p=ls(p);r=mid;
            ls(tot)=tot+1;
        }
        else{
            p=rs(p);l=mid+1;
            rs(tot)=tot+1;
        }
        tr[++tot]=tr[p];
    }
    s1(tot)=R[x];
    s2(tot)=C[x];
    p=tot-1;
    while(p>lst) Pushup(p--);
}
inline long long CalcD(const int &pos,const int &del){
    return min((long long)C[pos],1ll*del*R[pos]);
}
inline long long CalcL(const int &p1,const int &p2,const int &del){
    return (s1(p2)-s1(p1))*del+s2(p1);
}
int Query(const int &p1,const int &p2,const int &l,const int &r,const int lf,const int &rf,const int &del){
    if(l>rf||r<lf) return 0;
    const int mid=(lf+rf)>>1;
    if(l<=lf&&r>=rf){
        long long tmp;
        if(lf==rf){
            tmp=CalcD(lf,del);
            if(tmp>=rem){
                rem=tmp-rem;
                return rf;
            }
            else{
                rem-=tmp;
                return 0;
            }
        }
        tmp=CalcL(p1,p2,del);
        if(tmp>rem){
            const int ret=Query(ls(p1),ls(p2),l,r,lf,mid,del);
            if(ret) return ret;
            else return Query(rs(p1),rs(p2),l,r,mid+1,rf,del);
        }
        else{
            rem-=tmp;
            if(rem) return 0;
            else return rf;
        }
    }
    int ret=0;
    if(lf<=mid) ret=Query(ls(p1),ls(p2),l,r,lf,mid,del);
    if(ret) return ret;
    else if(rf>mid) return Query(rs(p1),rs(p2),l,r,mid+1,rf,del);
    else return 0;
}
int main(){
    scanf("%d",&n);
    for(i=1;i<=n;++i){
        scanf("%d%d",C+i,R+i);
        N[i]={R[i],C[i],L[i]=C[i]/R[i],i};
    }
    sort(N+1,N+n+1);
    for(i=1;i<=n;++i) Insert(N[i].idx);
    for(i=n;i;--i) S[++top]={i,i,1,0,C[i]};
    scanf("%d",&q);
    while(q--){
        scanf("%d%lld",&t,&rem);
        lc=1;rc=0;
        while(top){
            now=S[top--];
            if(now.le==1){
                now.ma+=1ll*(t-now.ti)*R[now.l];
                now.ma=min(now.ma,(long long)C[now.l]);
                if(now.ma>=rem){
                    if(now.ma>rem){
                        now.ma-=rem;
                        now.ti=t;
                        S[++top]=now;
                    }
                    else rc=now.r;
                    if(rc) S[++top]={lc,rc,rc-lc+1,t,0};
                    rem=0;break;
                }
                else{
                    rc=now.r;
                    rem-=now.ma;
                }
            }
            else{
                de=t-now.ti;
                pt=lower_bound(val+1,val+n+1,de)-val-1;
                rb=Query(rt[pt],rt[tim],now.l,now.r,1,n,de);
                if(rb){
                    if(rb<now.r) S[++top]={rb+1,now.r,now.r-rb,now.ti,0};
                    if(rem){
                        S[++top]={rb,rb,1,t,rem};
                        S[++top]={lc,rb-1,rb-lc,t,0};
                    }
                    else S[++top]={lc,rb,rb-lc+1,t,0};
                    rem=0;break;
                }
                else rc=now.r;
            }
        }
        if(rem){
            ans+=rem;
            S[top=1]={1,n,n,t,0};
        }
    }
    printf("%lld",ans);
    return 0;
}

Madoka and the Sixth-graders

题意

有一个有 \(n\) 个座位的教室,开始时编号为 \(1\sim n\) 的人在教室内,第 \(b_i\) 个人在第 \(i\) 号座位,教室外面有无数的编号为 \(n+1,n+2,\cdots\) 的人。每个人编号唯一。

每堂课结束后,下一堂课开始前,会依次发生如下事情:

  1. \(i\) 号座位的人会到第 \(p_i\) 号座位。
  2. 每个座位上的编号最小者留下,其余人永久离开。(保证此过程会发生)
  3. 不断有教室外面最小标号的人回到编号最小的空座位,直到教室内无空座位。

现在给出某堂课时每个座位的人和 \(p\),求字典序最小的合法 \(b\)\(n\le 10^5\)

解法

\(p\) 看成一张有向图,则由于有向图的没有入度的点确定,则每节课后进教室的人同样确定,故而不必考虑最后的编号大于 \(n\) 的人,他们的位置不会影响答案。

通过编号最大的人的编号和没有入度的点的数量可以判断进行的课的数量,使用倍增可以处理最初在某个点上的人在这些课后到达的点。

如果不考虑某些人的离开,则最后可以看成每个人一直在教室里,但是最后只知道每个点上编号最小的人的编号。同时由于两个人相遇后,他们会一直在同一个位置上,故而最后小于 \(n\) 且不为某个点上编号最小的编号对应的人一定最后和编号更小的人在一个位置上。

故而对于最后某个点上编号最小的编号对应的人,他们的位置是确定的,然后对于不确定的位置,按位置的编号从小到大依次选择编号最小的合法者安排即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
int n,i,j,p,u,mx,lev;
int to[33][maxn],c[maxn],r[maxn],k[maxn];
bool in[maxn];
inline int Log2(int x){
    int ret=0;
    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;
}
set<int> s;
set<int>::iterator it;
inline bool cmp(const int &x,const int &y){return c[x]<c[y];}
int main(){
    scanf("%d",&n);
    for(i=1;i<=n;++i){
        scanf("%d",&p);
        in[p]=1;
        to[0][i]=p;
    }
    for(i=1;i<=32;++i) for(j=1;j<=n;++j) to[i][j]=to[i-1][to[i-1][j]];
    for(i=1;i<=n;++i) if(!in[i]) ++lev;
    for(i=1;i<=n;++i){
        scanf("%d",c+i);
        mx=max(mx,c[i]);
    }
    lev=(mx-n)/lev;
    for(i=1;i<=n;++i){
        mx=lev;u=i;in[i]=0;
        while(mx){
            p=mx&-mx;
            u=to[Log2(p)][u];
            mx^=p;
        }
        s.insert(i);
        r[i]=u;
    }
    for(i=1;i<=n;++i){
        if(c[r[i]]<n){
            it=s.find(c[r[i]]);
            if(it!=s.end()){
                s.erase(it);
                k[i]=c[r[i]];
            }
        }
    }
    for(i=1;i<=n;++i){
        if(k[i]) continue;
        it=s.upper_bound(c[r[i]]);
        if(it!=s.begin()) --it;
        if(*it<c[r[i]]) ++it;
        k[i]=*it;s.erase(it);
    }
    for(i=1;i<=n;++i) printf("%d ",k[i]);
    return 0;
}

Madoka and Laziness

题意

有一个长为 \(n\) 的序列 \(a\),现在要把 \(a\) 分成两个子序列 \(a_1,a_2\),满足 \(a_1,a_2\) 均可划分为两个单调的子串。求所有不同的无序二元组 \(\{\max a_1,\max a_2\}\) 的数量。\(n\le 5\cdot 10^5\)

解法

考虑每个数分别在哪个序列中。

只考虑两个序列均递增的情况比较简单,记 \(dp_{1,i}\) 表示 \(a_i\) 在某个序列前缀的情况下另一个序列末尾的最小值。(此时两个序列的末尾分别为 \(a_i,dp_{1,i}\),显然 \(a_i\) 确定时另一个序列的末尾最小能继续接的数最多)转移有

\[dp_{1,i}=\min(\inf,\begin{cases}dp_{1,i-1}&a_i>a_{i-1}\\a_{i-1}&a_i>dp_{1,i-1}\end{cases}) \]

初值显然有 \(dp_{1,0}=-\inf\)。此时若 \(dp_{1,i}\) 不为 \(\inf\),则 \(a_1\sim a_i\) 存在一种划分方案,否则不存在。

同理,处理两个数后缀的递减情况,记 \(dp_{2,i}\) 表示 \(a_i\) 在某个序列后缀的情况下另一个序列前面的最小值(不存在记为 \(\inf\))。

由于 \(\max a\) 一定在某个序列中为最大值,可以从 \(\arg\max_{i=1}^na_i\) 开始(记为 \(p\))。若 \(a_p\) 所不在的序列的最大值的下标大于 \(p\)(记为 \(x\)),则 \(dp_{2,x}\) 不为 \(\inf\),对于这段设 \(dp_{3,i}\) 表示 \(a_i\) 在递增部分时递减部分末尾的最大值,\(dp_{4,i}\) 表示 \(a_i\) 在递减部分时递增部分末尾的最小值。

转移有:

\[dp_{3,i}=\max(-\inf,dp_{3,i-1}[a_i>a_{i-1}],a_{i-1}[a_i>dp_{4,i-1}]) \]

\[dp_{4,i}=\min(\inf,dp_{4,i-1}[a_i<a_{i-1}],a_{i-1}[a_i<dp_{3,i-1}]) \]

最后 \(\bold{dp_{3,i}>dp_{2,i}}\)\(i\) 即为一个合法的 \(x\)

\(a\) 序列翻转一遍再重复如上过程,即可以得到所有的答案。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=500010;
const int INF=1145141919;
int n,i,p,ans;
int a[maxn];
int dp1[maxn],dp2[maxn],dp3n[maxn],dp3x[maxn];
int main(){
    scanf("%d",&n);
    for(i=1;i<=n;++i){
        scanf("%d",a+i);
        if(a[i]>a[p]) p=i;
    }
    for(i=1;i<=p;++i){
        dp1[i]=INF;
        if(a[i]>a[i-1]) dp1[i]=dp1[i-1];
        if(a[i]>dp1[i-1]) dp1[i]=min(dp1[i],a[i-1]);
    }
    for(i=n-1;i>p;--i){
        dp2[i]=INF;
        if(a[i]>a[i+1]) dp2[i]=dp2[i+1];
        if(a[i]>dp2[i+1]) dp2[i]=min(dp2[i],a[i+1]);
    }
    dp3n[p]=dp1[p];dp3x[p]=-INF;
    for(i=p+1;i<=n;++i){
        dp3n[i]=INF;dp3x[i]=-INF;
        if(a[i]>a[i-1]) dp3x[i]=dp3x[i-1];
        if(a[i]<a[i-1]) dp3n[i]=dp3n[i-1];
        if(a[i]>dp3n[i-1]) dp3x[i]=max(dp3x[i],a[i-1]);
        if(a[i]<dp3x[i-1]) dp3n[i]=min(dp3n[i],a[i-1]);
        if(dp3x[i]>dp2[i]) ++ans;
    }
    reverse(a+1,a+n+1);
    p=n-p+1;
    for(i=1;i<=p;++i){
        dp1[i]=INF;
        if(a[i]>a[i-1]) dp1[i]=dp1[i-1];
        if(a[i]>dp1[i-1]) dp1[i]=min(dp1[i],a[i-1]);
    }
    for(i=n-1;i>p;--i){
        dp2[i]=INF;
        if(a[i]>a[i+1]) dp2[i]=dp2[i+1];
        if(a[i]>dp2[i+1]) dp2[i]=min(dp2[i],a[i+1]);
    }
    dp3n[p]=dp1[p];dp3x[p]=-INF;
    for(i=p+1;i<=n;++i){
        dp3n[i]=INF;dp3x[i]=-INF;
        if(a[i]>a[i-1]) dp3x[i]=dp3x[i-1];
        if(a[i]<a[i-1]) dp3n[i]=dp3n[i-1];
        if(a[i]>dp3n[i-1]) dp3x[i]=max(dp3x[i],a[i-1]);
        if(a[i]<dp3x[i-1]) dp3n[i]=min(dp3n[i],a[i-1]);
        if(dp3x[i]>dp2[i]) ++ans;
    }
    printf("%d",ans);
    return 0;
}

Edge Deletion

题意

有一张 \(n\) 个点 \(m\) 条边的简单无向图。现在要删去尽可能多的边,满足最后所有点两两间最短路长度不变,求删去的最多边数。\(n\le 300\)

解法

显然如果删除某一条边对其他的最短路没有影响(这条边不是其所连两点之间的唯一最短路),则可以把这条边删去,其不会影响其他任何的最短路。故而用 floyd 求最短路即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=310;
const int maxm=45010;
int n,m,u,v,w,k,i,j,ans;
int U[maxm],V[maxm],W[maxm];
ll dis[maxn][maxn];
int main(){
    scanf("%d%d",&n,&m);
    memset(dis,0x3f,sizeof(dis));
    for(i=1;i<=m;++i){
        scanf("%d%d%d",&u,&v,&w);
        dis[u][v]=dis[v][u]=w;
        U[i]=u;V[i]=v;W[i]=w;
    } 
    for(k=1;k<=n;++k) for(i=1;i<=n;++i) for(j=1;j<=n;++j) dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
    for(i=1;i<=m;++i){
        u=U[i];v=V[i];w=W[i];
        for(j=1;j<=n;++j){
            if(dis[u][j]+dis[j][v]<=w){
                ++ans;
                break;
            }
        }
    }
    printf("%d",ans);
    return 0;
}

Lottery

题意

\(n\) 种物品,每次抽中第 \(i\) 个物品的概率为 \(p_i\)。求抽 \(k\) 次物品,总共抽中 \(m\) 种物品的概率模 \(998244353\)\(n,m,k\le 50\)

解法

如果指定了第 \(i\) 种物品被抽中了 \(c_i\) 次,则概率为 \(\prod_{i=1}^np_i^{c_i}\frac{k!}{\prod_{i=1}^nc_i!}\),即 \(k!\prod_{i=1}^n\frac{p_i^{c_i}}{c_i!}\)。考虑拆分贡献。

\(dp_{i,j,k}\) 表示前 \(i\) 种物品中抽了 \(j\) 种,次数为 \(k\) 的概率除以 \(k!\),转移方程有:

\[dp_{i,j,k}=dp_{i-1,j,k}+\sum_{t=1}^kdp_{i-1,j-1,k-t}\frac{p_i^{t}}{t!} \]

最后的答案显然为 \(k!dp_{n,m,k}\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=55;
const ll md=998244353;
ll s,t,fac[maxn],inv[maxn];
int n,m,k,i,j,p,c,w[maxn];
int dp[maxn][maxn][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;
}
int main(){
    scanf("%d%d%d",&n,&m,&k);
    fac[0]=fac[1]=inv[0]=inv[1]=1;
    for(i=1;i<=50;++i){
        s=fac[i-1]*i;
        if(s>=md) s%=md;
        fac[i]=s;
    }
    inv[50]=Pow(fac[50],md-2);
    for(i=50;i;){
        s=inv[i]*i;
        if(s>=md) s%=md;
        inv[--i]=s;
    }
    s=0;
    for(i=1;i<=n;++i){
        scanf("%d",w+i);
        s+=w[i];
    }
    s=Pow(s,md-2);
    for(i=1;i<=n;++i) w[i]=(s*w[i]%md);
    dp[0][0][0]=1;
    for(i=0;i<n;++i){
        for(j=0;j<=m;++j){
            for(p=0;p<=k;++p){
                s=dp[i][j][p];dp[i+1][j][p]+=s;
                if(dp[i+1][j][p]>=md) dp[i+1][j][p]-=md;
                if(j==m) continue;
                for(c=p+1;c<=k;++c){
                    t=s*Pow(w[i+1],c-p);if(t>=md) t%=md;
                    t*=inv[c-p];if(t>=md) t%=md;
                    dp[i+1][j+1][c]+=t;
                    if(dp[i+1][j+1][c]>=md) dp[i+1][j+1][c]-=md;
                }
            }
        }
    }
    s=fac[k]*dp[n][m][k];
    if(s>=md) s%=md;
    printf("%lld",s);
    return 0;
}

Sqrt

题意

有一个栈,初始只有一个数 \(x\)。令当前栈顶为 \(t\),则可以选择 \([1,\lfloor\sqrt t\rfloor]\) 的数入栈。求像这样入栈 \(10^{100}\) 个数后所有可能形成的不同的栈的数量。\(t\) 组数据。\(t\le 20,x\le 9\cdot 10^{18}\)

解法

考虑栈中最后一位对于答案的影响。

\(dp_i\) 为栈末元素是 \(i\) 时,后面能够接的不同后缀数量。转移即有 \(dp_i=1+\sum_{j=2}^{\lfloor\sqrt i\rfloor}dp_j\)(写成这样表示 \(dp_1=1\))。最后 \(dp_x\) 即为答案。

考虑优化 \(dp_x\) 的计算,有

\[\begin{aligned}dp_x&=1+\sum_{j=2}^{\lfloor\sqrt x\rfloor}dp_j\\&=1+\lfloor\sqrt x\rfloor+\sum_{j=2}^{\lfloor\sqrt x\rfloor}\sum_{k=2}^{\lfloor\sqrt j\rfloor}dp_k\\&=1+\lfloor\sqrt x\rfloor+\sum_{k=2}^{\lfloor^4\!\!\!\sqrt x\rfloor}(\lfloor\sqrt x\rfloor-k^2+1)dp_k\end{aligned} \]

可以预处理 \(dp_2\sim dp_{65536}(65536^4>9\cdot10^{18})\),然后对 \(x>65536\) 按上述形式讨论即可。

具体实现可能有所不同。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
int t,i;
unsigned long long n,b,k,s,p,dp[65550],pre[65550],ans;
unsigned long long FuckCMathSqrt(const unsigned long long &ds){
    unsigned long long r=(1u<<31),m=0;
    while(r){
        m+=r;
        if(m*m>ds) m-=r;
        r>>=1ULL;
    }
    return m;
}
int main(){
    dp[1]=pre[1]=1;
    for(i=2;i<65536;++i){
        b=FuckCMathSqrt(i);dp[i]=pre[b];
        pre[i]=pre[i-1]+dp[i];
    }
    scanf("%d",&t);
    while(t--){
        scanf("%llu",&n);
        if(n<65536){
            printf("%llu\n",dp[n]);
            continue;
        }
        b=FuckCMathSqrt(n);
        if(b<65536){
            printf("%llu\n",pre[b]);
            continue;
        }
        k=65536;
        s=256;ans=pre[65535];
        while(k<=b){
            ++s;p=s*s;
            if(p>b) p=b+1;
            ans+=pre[s-1]*(p-k);
            k=p;
        }
        printf("%llu\n",ans);
    }
    return 0;
}

Vitaly and Advanced Useless Algorithms

题意

\(n\) 个任务,第 \(i\) 个任务要求在 \(a_i\) 时刻前完成。现在有 \(m\) 件事情可以做,第 \(i\) 个事情耗时 \(t_i\),使第 \(e_i\) 个任务完成百分比增加 \(p_i\,\%\)。每件事情最多只能被做一次,一个任务只要完成百分比不低于 \(100\,\%\) 则视为完成。现在求能否按时做完所有任务,若能则给出一组方案。\(t\) 组数据。\(t\le 10^4,n,m\le 10^5,\sum n+m\le 2\cdot 10^5\)

解法

显然贪心一下,将任务按 \(a_i\) 升序排序在依次完成,可以发现这是最可能构造合法解的方式。

至于任务的完成最小时间,可以将所有事件按照对应任务的 \(a_i\) 升序排序,使用 01 背包转移即可。由于背包的“总价值”(能完成任务的百分比)值域(小于 \(200\),否则可以发现不用完成最后一个事件)远小于“体积”(总时间)(高达 \(n\cdot10^9\)),所以交换“体积”和“价值”转移即可。在寻找可行方案时,可以用一个与背包转移的数组同步转移的数组(不进行内存优化),对其进行不断回溯,保存已经确定的任务完成方案即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
int t,n,m,e,p,i,j,k,c,ti,us,ki,ta,top;
int dp[200],pre[200],use[200];
int h[maxn],v[maxn],stk[maxn],ans[maxn];
bool tag[maxn][200];
struct tri{int tim,per,nxt;}E[maxn];
int main(){
    scanf("%d",&t);
    while(t--){
        us=ta=0;
        scanf("%d%d",&n,&m);
        for(i=1;i<=n;++i){
            scanf("%d",v+i);
            h[i]=0;
        }
        for(i=1;i<=m;++i){
            scanf("%d%d%d",&e,&ti,&p);
            E[i]={ti,p,h[e]};h[e]=i;
        }
        for(i=1;i<=n;++i){
            memset(dp,0x3f,sizeof(dp));
            if(!h[i]){
                printf("-1\n");
                ti=-1;break;
            }
            c=dp[0]=top=0;
            for(j=h[i];j;j=E[j].nxt){
                stk[++top]=j;
                p=E[j].per;ti=E[j].tim;
                for(k=199;k>=p;--k){
                    if(dp[k-p]+ti<dp[k]){
                        dp[k]=dp[k-p]+ti;
                        tag[j][k]=1;
                    }
                    else tag[j][k]=0;
                }
                while(k) tag[j][k--]=0;
            }
            ti=0x3f3f3f3f;
            for(k=100;k<200;++k){
                if(dp[k]<ti){
                    ti=dp[k];
                    ki=k;
                }
            }
            if(v[i]<ti+us){
                printf("-1\n");
                ti=-1;break;
            }
            us+=ti;
            while(top){
                j=stk[top--];
                if(tag[j][ki]){
                    ki-=E[j].per;
                    ans[++ta]=j;
                }
            }
        }
        if(~ti){
            printf("%d\n",ta);
            for(i=1;i<=ta;++i) printf("%d ",ans[i]);
            putchar('\n');
        }
    }
    return 0;
}

Counting Shortcuts

题意

有一个 \(n\) 个点 \(m\) 条边的简单无向连通图,每条边边权为 \(1\),求从 \(s\)\(t\) 的长度不超过从 \(s\)\(t\) 的最短路长度 \(+1\) 的路径条数。\(t\) 组数据。\(\sum n,m\le 2\cdot 10^5\)

解法

模板严格次短路题。注意在最短路算法时,维护到某个节点的次短路后同样需要将其压入所用队列中(其可能更新其他节点的次短路)。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int md=1000000007;
const int maxn=200010;
const int maxm=200010;
int t,n,m,u,v,w,x,y,s,i,tot;
int h[maxn],d[2][maxn],c[2][maxn];
bool l,vis[2][maxn];
struct edge{int to,nxt;}E[maxm<<1];
struct node{
    int now;
    bool len;
    int dis;
    inline bool operator <(const node &a)const{return dis>a.dis;}
};
priority_queue<node> q;
int main(){
    scanf("%d",&t); 
    while(t--){
        scanf("%d%d%d%d",&n,&m,&x,&y);
        for(i=1;i<=n;++i){
            d[0][i]=d[1][i]=0x3f3f3f3f;
            h[i]=c[0][i]=c[1][i]=vis[0][i]=vis[1][i]=0;
        }
        while(m--){
            scanf("%d%d",&u,&v);
            E[++tot]={v,h[u]};h[u]=tot;
            E[++tot]={u,h[v]};h[v]=tot;
        }
        d[0][x]=0;c[0][x]=1;
        q.push((node){x,0,0});
        while(!q.empty()){
            u=q.top().now;
            s=q.top().dis+1;
            l=q.top().len;
            q.pop();
            if(vis[l][u]) continue;
            vis[l][u]=1;
            for(i=h[u];i;i=E[i].nxt){
                v=E[i].to;
                if(s<d[0][v]){
                    d[1][v]=d[0][v];c[1][v]=c[0][v];
                    q.push((node){v,1,d[1][v]});
                    d[0][v]=s;c[0][v]=c[l][u];
                    q.push((node){v,0,s});
                }
                else if(s==d[0][v]){
                    c[0][v]+=c[l][u];
                    if(c[0][v]>=md) c[0][v]-=md;
                }
                else if(s<d[1][v]){
                    d[1][v]=s;c[1][v]=c[l][u];
                    q.push((node){v,1,d[1][v]});
                }
                else if(s==d[1][v]){
                    c[1][v]+=c[l][u];
                    if(c[1][v]>=md) c[1][v]-=md;
                }
            }
        }
        if(d[1][y]==d[0][y]+1){
            c[0][y]+=c[1][y];
            if(c[0][y]>=md) c[0][y]-=md;
        }
        printf("%d\n",c[0][y]);
        tot=0;
    }
    return 0;
}

Distinct Numbers

题意

有一个长为 \(n\) 的数组 \(a\),保证 \(a\) 中的数为两两不同的非负整数。现在先后手轮流对 \(a\) 进行如下操作:把 \(a\) 中最大值改为一个更小的,与目前 \(a\) 中其他数均不相同的非负整数,无法操作者判负。现在先手和后手均以最优策略操作,求获胜者。\(n\le 3\cdot 10^5,0\le a_i\le 10^9\)

解法

直接判断胜负性规律十分困难,考虑通过打表、手动判断等方式判断。发现后手必胜当且仅当 \(a_{n}-a_{n-1}=1\)\(a_n\)\(n\) 奇偶性不同。

证明:

首先题中没有操作的情况有且只有序列变为 \(\{0,1,2,\cdots,n-1\}\),此情况显然后手必胜,同时也满足 \(a_{n}-a_{n-1}=1\)\((a_n+n)\!\!\!\mod\!2=1\)。考虑对于任意一种上述条件不全满足的情况,操作者是否可以把它转为同时满足两种条件的情况。(下面令 \(a\) 数组始终是升序排好序的)

如果 \((a_n+n)\!\!\!\mod\!2=0\),则若 \(a_n-a_{n-1}\ne 1\),可以把 \(a_n\) 转为 \(a_n-1\),否则必有 \((a_{n-1}+n)\!\!\!\mod\!2=1\),又因为 \(a_n\ne n-1\),所以可以把 \(a_n\) 变为任意一个满足条件的数即可。

如果 \(a_n-a_{n-1}\ne1\)\((a_n+n)\!\!\!\mod\!2=1\),则如果 \((a_{n-1}+n)\!\!\!\mod\!2=0\),则将 \(a_n\) 变为 \(a_{n-1}+1\),否则必有 \(a_{n-1}>n-2\),若 \(a_{n-2}=a_{n-1}-1\) 则将 \(a_n\) 变为小于 \(a_{n-2}\) 的任意一个数,否则将 \(a_n\) 变为 \(a_{n-1}-1\)

同时 \(a_{n}-a_{n-1}=1\)\((a_n+n)\!\!\!\mod\!2=1\) 时先手无论怎么操作,新的数组的 \(a_n\) 一定满足 \((a_n+n)\!\!\!\mod\!2=0\),后手可按照上述方法操作即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=300010;
int n,i,a[maxn];
int main(){
    scanf("%d",&n);
    for(i=1;i<=n;++i) scanf("%d",a+i);
    if(((a[n]^n)&1)&&(a[n]==a[n-1]+1)) printf("Bob\n");
    else printf("Alice\n");
    return 0;
}

Prefix XORs

题意

有一个长为 \(n\) 的数组 \(a\)。现在进行 \(m\) 次操作,每次操作将 \(a_i\) 变为 \(\bigoplus_{j=1}^i a_j\)。求每次操作后的 \(a_n\)\(n,m\le 10^6,a_i<2^{30}\)

解法

听说 GaryH 15 分钟爆切此题

考虑把第 \(i\) 次操作后的数组 \(A(i)\) 用原数组 \(a\) 表示,有:

  • \(A(1)=\{\bigoplus_{i=1}^1a_i,\bigoplus_{i=1}^2a_i,\cdots,\bigoplus_{i=1}^na_i\}\)
  • \(A(2)=\{\bigoplus_{i=1}^1a_i,\bigoplus_{i=1}^2([(2-i+1)\!\!\!\mod\!2=1]a_i),\cdots,\bigoplus_{i=1}^n([(n-i+1)\!\!\!\mod\!2=1]a_i)\}\)
  • \(\huge{\cdots\cdots}\)
  • \(A(m)=\{\bigoplus_{i=1}^1([\binom{m-i}{1-i}\!\!\!\mod\!2=1]a_i),\bigoplus_{i=1}^2([\binom{m+1-i}{2-i}\!\!\!\mod\!2=1]a_i),\cdots,\bigoplus_{i=1}^n([\binom{m+n-i-1}{n-i}\!\!\!\mod\!2=1]a_i)\}\)

所以最后第 \(k\) 次操作的 \(A(k)_n\) 即为 \(\bigoplus_{i=1}^n([\binom{k+n-i-1}{n-i}\!\!\!\mod\!2=1]a_i)\),考虑将 \(\binom{k+n-i-1}{n-i}\) 拆开为 \(\frac{(k+n-i-1)!}{(n-i)!(k-1)!}\),此时需要统计 \((k+n-i-1)!\) 等含有的质因子 \(2\) 的个数。

\(a=n-i,b=k-1\),记 \(f(k)=\sum_{i=1}\lfloor\frac{k}{2^i}\rfloor\),则显然 \(f(k)\)\(k!\) 含有的质因子 \(2\) 的个数,则 \(\frac{(k+n-i-1)!}{(n-i)!(k-1)!}\!\!\!\mod\!2=1\) 当且仅当 \(f(a+b)=f(a)+f(b)\),即 \(\sum_{i=1}(\lfloor\frac{a+b}{2^i}\rfloor-\lfloor\frac{a}{2^i}\rfloor-\lfloor\frac{b}{2^i}\rfloor)\)。由于 \(\lfloor\frac{a+b}{2^i}\rfloor-\lfloor\frac{a}{2^i}\rfloor-\lfloor\frac{b}{2^i}\rfloor\ge 0\)\(\lfloor\frac{a+b}{2^i}\rfloor-\lfloor\frac{a}{2^i}\rfloor-\lfloor\frac{b}{2^i}\rfloor=0\) 当且仅当 \(a\text{ mod }2^i+b\text{ mod }2^i<2^i\)\(i=1\) 时满足上述不等关系的充要条件是 \(a\text{ mod }2=b\text{ mod }2=0\),通过数学归纳法可以证明 \(f(a+b)=f(a)+f(b)\) 的充要条件是 \(a\text{ AND }b=0\)

\(dp_{i,j}\) 表示考虑 \(j\) 的二进制下最后 \(i\) 位,然后 \(\forall k\in[1,n]\),满足 \(\lfloor\frac{k}{2^i}\rfloor=\lfloor\frac{j}{2^i}\rfloor\),且 \((k{\text{ mod }}2^i){\text{ AND }}(j{\text{ mod }}2^i)=0\)\(\bigoplus a_k\)。若 \(j\text{ AND }2^i=0\)\(dp_{i+1,j}=dp_{i,j}\oplus dp_{i,j\oplus2^i}\),否则 \(dp_{i+1,j}=dp_{i,j\oplus2^i}\)。初值为 \(dp_{0,i}=a_{n-i}\),最后 \(dp_{20,i}\) 即为第 \(i\) 次操作后的 \(a_n\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2097152;
int n,m,i,j,dp[21][maxn];
int main(){
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;++i) scanf("%d",dp[0]+n-i);
    for(i=0;i<20;++i){
        for(j=0;j<maxn;++j){
            dp[i+1][j]=dp[i][j^(1<<i)];
            if(!(j&(1<<i))) dp[i+1][j]^=dp[i][j];
        }
    }
    for(i=0;i<m;++i) printf("%d ",dp[20][i]);
    return 0;
}

Shortest Good Path

题意

有一张 \(n\) 个点 \(m\) 条边的简单无向连通图。定义一条路径(可为空)的权值为 \(\bigcup_{i=1}^n [\footnotesize\texttt{点 i 被经过了奇数次}]\,2^{i-1}\)。现在对每个 \([0,2^n)\) 的二进制数 \(s\),记 \(f(s)\) 为权值为 \(s\) 的路径经过的最少点数。求 \(\sum_{i=0}^{2^n-1}f(i)\)\(n\le 17\)

解法

如果把每条路径权值的具体二进制表示看成结点,把从某结点到另一结点对路径权值的变化看作有向边,则此题即可视为 bfs 求最短路。注意维护每条路径的终点,因为终点不一样决定了之后延伸的路径的权值的不一样。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=18;
const int max2=131100;
int n,m,i,j,u,v,w;
bool d[maxn][maxn],dis[max2],lst[max2][maxn];
struct node{int now,dis,msk;};
queue<node> q;
long long ans;
int main(){
    scanf("%d%d",&n,&m);
    for(i=1;i<=m;++i){
        scanf("%d%d",&u,&v);
        d[u][v]=d[v][u]=1;
    }
    ans=n;dis[0]=1;
    for(i=1;i<=n;++i){
        lst[0][i]=1;
        v=1<<(i-1);dis[v]=lst[v][i]=1;
        q.push((node){i,1,v});
    }
    while(!q.empty()){
        u=q.front().now;
        m=v=q.front().msk;
        w=q.front().dis+1;
        q.pop();
        for(i=1;i<=n;++i){
            if(d[u][i]){
                m=v^(1<<(i-1));
                if(lst[m][i]) continue;
                if(!dis[m]) ans+=w;
                dis[m]=lst[m][i]=1;
                q.push((node){i,w,m});
            }
        }
    }
    printf("%lld",ans);
    return 0;
}

Construct Good Path

题意

有一张 \(n\) 个点 \(m\) 条边的简单无向连通图。定义一条路径(可为空)的权值为 \(\bigcup_{i=1}^n [\footnotesize\texttt{点 i 被经过了奇数次}]\,2^{i-1}\)。现在给出一个二进制数 \(s\)(以二进制形式表示出),求经过的边少于 \(4n\) 的任意一条权值为 \(s\) 的路径。\(n\le 10^5\)

解法

考虑如果整个图变成一棵树(或是随机生成一棵 dfs 树),则对于任意权值是否存在一条路径。

显然将整棵树 dfs 一遍可以把每条边刚好经过两次,此时形成了某个权值的路径。这时若将某条边经过两次,则显然会把边上的两个端点各多经过一次。比较 dfs 出的路径权值与给定权值的差异,自下而上选择每条边是否多被经过两次即可。若最后根结点的经过次数不满足要求,则直接不走 dfs 的最后一条边即可。最后这条路径经过的边的数量显然不会超过 \(4n\)

代码的实现可能略有不同。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
const int maxm=400010;
int n,m,u,v,t,i,tl,tim;
int h[maxn],in[maxn],fa[maxn],up[maxn];
int prv[maxn<<2],nxt[maxn<<2],val[maxn<<2];
bool app[maxn],vis[maxn];
struct edge{int to,nxt;}E[maxm];
char s[maxn];
queue<int> q;
void dfs(const int &p){
    val[++tim]=p;
    nxt[tim]=tim+1;
    app[p]^=1;vis[p]=1;
    int lp,to;
    for(lp=h[p];lp;lp=E[lp].nxt){
        to=E[lp].to;
        if(vis[to]) continue;
        ++in[p];
        fa[to]=p;
        up[to]=tim+1;
        dfs(to);
        val[++tim]=p;
        nxt[tim]=tim+1;
        app[p]^=1;
    }
}
inline void Insert(const int &p,const int &v){
    val[++tim]=v;
    nxt[tim]=nxt[p];
    nxt[p]=tim;
    if(p==tl) tl=tim;
}
int main(){
    scanf("%d%d",&n,&m);
    while(m--){
        scanf("%d%d",&u,&v);
        E[++t]={v,h[u]};h[u]=t;
        E[++t]={u,h[v]};h[v]=t; 
    }
    scanf("%s",s+1);
    for(i=1;i<=n;++i){
        if(s[i]=='1'){
            i=0;
            break;
        }
    }
    if(i){
        printf("0\n");
        return 0;
    }
    dfs(1);
    nxt[tim--]=0;nxt[tim]=0;
    tl=tim;app[1]^=1;
    for(i=1;i<=n;++i) if(!in[i]) q.push(i);
    while(!q.empty()){
        u=q.front();q.pop();
        if(u==1) continue;
        if(app[u]!=(s[u]-'0')){
            app[u]^=1;
            app[fa[u]]^=1;
            Insert(up[u],u);
            Insert(up[u],fa[u]);
        }
        if(!(--in[fa[u]])) q.push(fa[u]);
    }
    if(app[1]!=(s[1]-'0')){
        ++tim;
        val[tim]=1;
        nxt[tl]=tim;
    }
    printf("%d\n",tim);
    for(i=1;i;i=nxt[i]) printf("%d ",val[i]);
    return 0;
}

Minimal String Xoration

题意

有一个长为 \(2^n\) 的字符串 \(s=s_0s_1\cdots s_{2^n-1}\)。令 \(f(j)(j\in[0,2^n))=s_{0\oplus j}s_{1\oplus j}\cdots s_{(2^n-1)\oplus j}\),求字典序最小的 \(f(j)\)\(n\le 18\)

解法

听说理解后缀排序有助于理解下面的内容。

考虑比较两个字符串 \(s,t(|s|=|t|=2^k)\) 的一种方式:可以先比较 \(s_0\sim s_{2^{k-1}-1}\)\(t_0\sim t_{2^{k-1}-1}\),两者无大小差距后再比较 \(s_{2^{k-1}}\sim s_{2^k-1}\)\(t_{2^{k-1}}\sim t_{2^k-1}\)

同时发现如果只考虑每一个 \(f(i)\) 的前 \(2^k\) 位,则可以发现

\[\begin{aligned}&{\color{white}{=}\ \ }f(i)_{2^k}\sim f(i)_{2^{k+1}-1}\\&=s_{2^k\oplus i}\sim s_{(2^{k+1}-1)\oplus i}\\&=s_{0\oplus (i\oplus 2^k)}\sim s_{(2^k-1)\oplus(i\oplus 2^k)}\\&=f(i\oplus 2^k)_0\sim f(i\oplus 2^k)_{2^k-1}\end{aligned} \]

故而在每一次增大 \(k\) 的同时将 \(\{0,1,\cdots,2^n-1\}\) 按照 \(f(i)_{0}\sim f(i)_{2^{k+1}-1}\) 升序排好序即可(注意保存它们按照 \(f(i)_{0}\sim f(i)_{2^k-1}\) 排序的结果)。

时间复杂度为 \(O(n^2{2^n})\)。但是可以发现上述排序过程为双关键字排序(值域也较小),由此可以使用基数排序,时间复杂度可以降为 \(O(n2^n)\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=262150;
int n,i,j,l,c,dlt;
int rnk[maxn],ner[maxn],pla[maxn];
char s[maxn];
inline bool cmp(const int &x,const int &y){
    if(rnk[x]!=rnk[y]) return rnk[x]<rnk[y];
    return rnk[x^dlt]<rnk[y^dlt];
}
int main(){
    scanf("%d%s",&n,s);
    l=1<<n;
    for(i=0;i<l;++i){
        rnk[i]=s[i]-'a';
        pla[i]=i;
    }
    sort(pla,pla+l,cmp);
    for(i=0;i<n;++i){
        dlt=1<<i;
        sort(pla,pla+l,cmp);
        ner[pla[0]]=c=1;
        for(j=1;j<l;++j){
            if(cmp(pla[j-1],pla[j])) ++c;
            ner[pla[j]]=c;
        }
        for(j=0;j<l;++j) rnk[j]=ner[j];
    }
    dlt=pla[0];
    for(i=0;i<l;++i) putchar(s[i^dlt]);
    return 0;
}

Words on Tree

题意

有一个大小为 \(n\) 的树。现在需要在树上每个点填一个字符,但是有 \(q\) 组限制,第 \(i\) 组限制为从 \(u_i\)\(v_i\) 的唯一简单路径上的所有字母组成的子串正着读或倒着读恰好为 \(s_i\)。求给每个点填字符的一组合法方案或判断为无解。\(n,q\le 4\cdot 10^5,\sum_{i=1}^q|s_i|\le 4\cdot 10^5\)

解法

首先要求的路径之间会有一些限制。把每条给出路径的正反方向看成是每个要求的赋值,则可以把每个要求看成是一个有点权且为 \(0\)\(1\) 的结点,然后某些要求有限制(某些要求的赋值会有冲突),把这些限制看成有向边。这样原问题转为了 2-SAT 问题,但是边数可能多达 \(O(n^2)\)

所以我们不把所有要求对应的点之间进行连边,而是把要求与树上结点连边。

如果把树上的每个结点对应为图中的点,则树上的结点有 \(26\) 种取值,同时每个要求和树上的结点有对应的赋值限制(如果某个点赋了某个值,则另外某个点一定只能赋为对应的某个值)。虽然边数降为 \(O(n)\) 条,但是这个问题转化为了 26-SAT 问题。

考虑树上某个点的不会与某个要求的路径的两个方向均冲突的赋值有多少种。某一个结点如果有一个合法的赋值,当且仅当所有经过它的路径均对应了一种合法的方向。也就是说,某一个结点的合法赋值,一定会在依次考虑每个限制的两个方向后的前两种赋值方法之一(或者是一定会满足第一种限制的路径对应的两种赋值之一)。

最后每个结点的合法赋值方案数不超过 \(2\) 种,可以直接用 2-SAT 问题解法进行解决。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=400010;
const int maxx=800020;
const int max2=1600040;
int n,q,i,j,u,v,c,x,y,t,l,r,top;
int h[maxn],fa[maxn],dep[maxn],s[max2];
int stk[max2],scc[max2],dfn[max2],low[max2];
struct edge{int to,nxt;}E[maxx],S[max2];
char ch,st[maxn],c0[maxn],c1[maxn];
bool in[max2];
void dfs(const int &p){
    int lp,to;
    for(lp=h[p];lp;lp=E[lp].nxt){
        to=E[lp].to;
        if(to==fa[p]) continue;
        fa[to]=p;dep[to]=dep[p]+1;
        dfs(to);
    }
}
inline void Link0(int &fuck){
    if(!c0[fuck]) c0[fuck]=ch;
    if(c0[fuck]!=ch){
        if(!c1[fuck]) c1[fuck]=ch;
        S[++t]={fuck+maxx,s[i]};s[i]=t;
        S[++t]={i+maxx,s[fuck]};s[fuck]=t;
    }
    if(c1[fuck]!=ch){
        S[++t]={fuck,s[i]};s[i]=t;
        S[++t]={i+maxx,s[fuck+maxx]};s[fuck+maxx]=t;
    }
    fuck=fa[fuck];
}
inline void Link1(int &fuck){
    if(!c0[fuck]) c0[fuck]=ch;
    if(c0[fuck]!=ch){
        if(!c1[fuck]) c1[fuck]=ch;
        S[++t]={i,s[fuck]};s[fuck]=t;
        S[++t]={fuck+maxx,s[i+maxx]};s[i+maxx]=t;    
    }
    if(c1[fuck]!=ch){
        S[++t]={i,s[fuck+maxx]};s[fuck+maxx]=t;
        S[++t]={fuck,s[i+maxx]};s[i+maxx]=t;
    }
    fuck=fa[fuck];
}
void tarjan(const int &p){
    dfn[p]=low[p]=++t;
    stk[++top]=p;in[p]=1;
    int lp,to;
    for(lp=s[p];lp;lp=S[lp].nxt){
        to=S[lp].to;
        if(!dfn[to]){tarjan(to);low[p]=min(low[p],low[to]);}
        else if(in[to]) low[p]=min(low[p],dfn[to]);
    }
    if(dfn[p]==low[p]){
        ++c;
        do{
            to=stk[top--];
            in[to]=0;
            scc[to]=c;
        }while(to!=p);
    }
}
int main(){
    scanf("%d%d",&n,&q);
    for(i=1;i<n;++i){
        scanf("%d%d",&u,&v);
        E[++t]={v,h[u]};h[u]=t;
        E[++t]={u,h[v]};h[v]=t;
    }
    dfs(dep[1]=1);t=0;
    for(i=1;i<=q;++i){
        i+=maxn;
        scanf("%d%d",&u,&v);
        x=u;y=v;scanf("%s",st+1);
        l=1;r=strlen(st+1);
        if(dep[u]<dep[v]){
            swap(u,v);
            reverse(st+1,st+r+1);
        }
        while(dep[u]>dep[v]){ch=st[l++];Link0(u);}
        while(u!=v){
            ch=st[l++];Link0(u);
            ch=st[r--];Link0(v);
        }
        ch=st[l];Link0(u);
        u=x;v=y;l=1;r=strlen(st+1);
        reverse(st+1,st+r+1);
        if(dep[u]<dep[v]) swap(u,v);
        while(dep[u]>dep[v]){ch=st[l++];Link1(u);}
        while(u!=v){
            ch=st[l++];Link1(u);
            ch=st[r--];Link1(v);
        }
        ch=st[l];Link1(u);
        i-=maxn;
    }
    for(i=1;i<=n;++i){
        if(!c1[i]) c1[i]='x';
        if(!c0[i]) c0[i]='x';
    }
    t=0;
    for(i=1;i<max2;++i) if(!dfn[i]) tarjan(i);
    for(i=1;i<maxx;++i){
        if(scc[i]==scc[i+maxx]){
            printf("NO\n");
            return 0;
        }
    }
    printf("YES\n");
    for(i=1;i<=n;++i){
        if(scc[i]<scc[maxx+i]) putchar(c0[i]);
        else putchar(c1[i]);
    }
    return 0;
}

Parametric MST

题意

有一个长为 \(n\) 的数组 \(a\)。定义 \(f(t)\) 为一张 \(n\) 个点的无向完全图(其中第 \(i\) 个点和第 \(j\) 个点之间连有边权为 \(a_i\cdot a_j+(a_i+a_j)\cdot t\) 的边)的最小生成树的边权和。求 \(f(t)\) 的最大值或输出其为正无穷。\(t\) 组数据。\(t\le 10^4,\sum n\le 2\cdot 10^5\)

解法

考虑 \(a_i\cdot a_j+(a_i+a_j)\cdot t=(a_i+t)(a_j+t)-t^2\)。我们可以把 \(a_i+t\) 看作 \(i\) 的新权值 \(f_i\),则每一条边 \((i,j)\) 的边权大小关系只与 \(f_i{f_j}\) 的大小关系有关。

考虑每个点的所有出边最小边权。记 \(f\) 值最小的点为 \(x\),最大的点为 \(y\)。显然某个点的 \(f\) 值为正,则最小边权的边连向 \(x\),某个点的 \(f\) 值为负,则最小边权的边连向 \(y\)。(此时即使 \(f\) 值全为正或全为负,有了自环,也不会影响)。可以发现所有的这些最小边的边集为一棵树(有可能 \((x,y)\) 边被选了两次,但是也不重要),由 kruskal 算法相关知识可以知道其是最小生成树。

同时可以先维护 \(t\) 值趋近于 \(\infty\)\(-\infty\) 的最小生成树的边权和。把它们表示成 \(a+bt\) 的形式,可以判断解是否为 \(\infty\) 的情况。同时发现 \(t\) 值递增时,每次的最小生成树的边最多会改变 \(1\) 条边。我们故而可以在 \(t\) 值递增时维护最小生成树的边权和,同样用 \(a+bt\) 的形式表示,顺便维护这个最大值即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
int t,n,i;
long long tn,mn,mx,nv,pv,ans;
long long a[maxn],pre[maxn];
int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        for(i=1;i<=n;++i) scanf("%lld",a+i);
        sort(a+1,a+n+1);
        for(i=1;i<=n;++i) pre[i]=pre[i-1]+a[i];
        if(((pre[n]+(n-2)*a[n])<0)||((pre[n]+(n-2)*a[1])>0)){
            printf("INF\n");
            continue;
        }
        ans=-9e18;
        for(i=n;i;--i){
            tn=-a[i];
            mn=a[1]+tn;mx=a[n]+tn;
            nv=pre[i-1]+tn*(i-1);
            pv=(pre[n]-pre[i])+tn*(n-i);
            ans=max(ans,nv*mx+pv*mn-mx*mn-(n-1)*tn*tn);
        }
        printf("%lld\n",ans);
    }
}

Gojou and Matrix Game

题意

有一个 \(n\times n\) 的矩阵 \(a\)。现在先手和后手轮流在其上做如下操作:选择与上一个人选择的格子曼哈顿距离超过 \(k\) 的任意一个格子 \((i,j)\),获得 \(a_{i,j}\) 的分数。先手和后手均会做 \(10^{100}\) 次上述操作,得分多者获胜。对于每一个格子,求先手选择此格子后,若先手和后手均按照最优策略操作,则最终的胜者。\(n\le 2000,k\le n-2\)\(a_{i,j}\)\([1,n^2]\) 之间互不相同的数。

解法

显然先手开始时选择 \(a\) 值最大的一定必胜。

考虑先手开始选择哪些位置时必胜。显然如果先手选择了某个位置 \((x,y)\),后手可以由此选到必胜位置(存在一个与 \((x,y)\) 曼哈顿距离超过 \(k\) 格的必胜位置),则由于两个人的操作均足够多,所以先手选择的这个位置一定不是必胜位置。

\(a\) 值最大的位置一定是必胜位置,故而可以把所有位置按照对应的 \(a\) 值降序排序,依次确定每个位置是否是必胜位置。(显然 \(a\) 值比目前这个位置小的其他位置不会影响到这个位置)

现在问题转化成了如何具体维护每个确定下来的必胜位置和一个新的位置的曼哈顿距离的最大值。

\[\begin{aligned}dis(i,j)&=|x_i-x_j|+|y_i-y_j|\\&={\begin{cases}(x_i+y_i)-(x_j+y_j)&x_i>x_j,y_i>y_j\\(x_i-y_i)-(x_j-y_j)&x_i>x_j,y_i\le y_j\\(x_j-y_j)-(x_i-y_i)&x_i\le x_j,y_i>y_j\\(x_j+y_j)-(x_i+y_i)&x_i\le x_j,y_i\le y_j\end{cases}}\\&=\max((x_i+y_i)-(x_j+y_j),(x_i-y_i)-(x_j-y_j),(x_j-y_j)-(x_i-y_i),(x_j+y_j)-(x_i+y_i))\\&=\max(|(x_i+y_i)-(x_j+y_j)|,|(x_i-y_i|-(x_j-y_j))\end{aligned} \]

故而可以保存先前所有位置的 \(x-y\)\(x+y\) 的最值即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2010;
int n,k,i,j,a,t,ln,lx,rn,rx;
bool dp[maxn][maxn];
struct node{
    int v,x,y,sx,sy;
    inline bool operator <(const node &a)const{return v>a.v;}
}N[maxn*maxn];
inline int Abs(const int &t){return t>0?t:-t;}
inline bool chk(const int &sx,const int &sy){
    if(Abs(ln-sx)>k) return 0;
    if(Abs(lx-sx)>k) return 0;
    if(Abs(rn-sy)>k) return 0;
    if(Abs(rx-sy)>k) return 0;
    return 1;
}
int main(){
    scanf("%d%d",&n,&k); 
    for(i=1;i<=n;++i){
        for(j=1;j<=n;++j){
            scanf("%d",&a);
            N[++t]={a,i,j,i+j,i-j};
        }
    }
    sort(N+1,N+t+1);
    dp[N[1].x][N[1].y]=1;
    ln=lx=N[1].sx;
    rn=rx=N[1].sy;
    for(i=2;i<=t;++i){
        if(!chk(N[i].sx,N[i].sy)) continue;
        dp[N[i].x][N[i].y]=1;
        if(N[i].sx<ln) ln=N[i].sx;
        else if(N[i].sx>lx) lx=N[i].sx;
        if(N[i].sy<rn) rn=N[i].sy;
        else if(N[i].sy>rx) rx=N[i].sy;
    }
    for(i=1;i<=n;++i){
        for(j=1;j<=n;++j) putchar(dp[i][j]?'M':'G');
        putchar('\n');
    }
    return 0;
}

Juju and Binary String

题意

定义一个 \(01\) 串的价值为其中 \(1\) 的数量与串长度的比值。现在给出一个长为 \(n\) 的字符串 \(s\),要从中选出最少不交的子串,满足它们拼接在一起的串长度恰好为 \(m\),且其价值等于 \(s\) 的价值。求一种最优方案或判断其为无解。\(t\) 组数据。\(t\le 10^4,n,m\le 2\cdot 10^5\)

解法

显然最后串中需要取的 \(0\)\(1\) 数量不是整数即无解。

考虑这个 \(01\) 串为环状时是否能够恰好只取一段。

\(f_i=\sum_{j=i}^{i+m-1}s_{j\text{ mod }n}\)\(s\) 下标为 \(0\sim n-1\)),\(S=\sum_{i=0}^{n-1}s_i\)\(A=\frac mn \sum_{i=0}^{n-1}s_i\)。显然 \(\sum_{i=0}^{n-1}f_i=mS\)。同时由于 \(\forall i\in[0,n-2],|f_{i+1}-f_i|\le 1\),故而若 \(\exists f_i<A,f_j>A\),则必 \(\exists p\in(i,j),f_p=A\)。同时肯定不存在 \(\forall i\in[0,n-1],f_i>A\)\(\forall i\in[0,n-1],f_i<A\) 的情况。

上述问题证实了在这个环中始终会有满足要求的一段,转化为链式 \(01\) 串时遍历 \([0,n-1]\),找到 \(\arg\min_{i=0}^{n-1}f_i=A\) 即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
int t,n,m,i,x,sm,hd,tl;
char s[maxn<<1];
int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d%d%s",&n,&m,s+1);sm=0;
        for(i=1;i<=n;++i) sm+=(s[i]=='1');
        if((1LL*sm*m)%n){
            printf("-1\n");
            continue;
        }
        x=(1LL*sm*m)/n;sm=0;
        memcpy(s+n+1,s+1,n);
        for(i=1;i<=m;++i) sm+=(s[i]=='1');
        hd=1;tl=m;
        while(tl<=n){
            if(sm==x){
                printf("1\n%d %d\n",hd,tl);
                tl=0;break;
            }
            if(s[hd++]=='1') --sm;
            if(s[++tl]=='1') ++sm;
        }
        if(!tl) continue;
        for(;;){
            if(sm==x){
                printf("2\n1 %d\n%d %d\n",tl-n,hd,n);
                break;
            }
            if(s[hd++]=='1') --sm;
            if(s[++tl]=='1') ++sm;
        }
    }
    return 0;
}

Wrapping Chocolate

题意

\(n\) 个矩形,第 \(i\) 个矩形长为 \(a_i\),宽为 \(b_i\);同时有 \(m\) 个矩形盒子,第 \(i\) 个盒子长为 \(c_i\),宽为 \(d_i\)。现在要将每个矩形单独放进一个盒子,且第 \(i\) 个矩形能被第 \(j\) 个盒子装下的充要条件为 \(a_i\le c_j,b_i\le d_j\)。求能否对每个矩形找到对应的盒子。巧克力和盒子不能旋转。\(n,m\le 2\cdot 10^5\)

解法

假设每个巧克力和盒子的 \(a_i\)\(c_j\) 相等的情况。考虑贪心,对于每个巧克力 \(i\) 选择 \(d_j>b_i\)\(d_j\) 最小的盒子 \(j\)

但是实际情况中,可能这个盒子的 \(c_j\) 是所有盒子中唯一的大于之后某块巧克力的 \(a\) 值的盒子,同时可能有其他的盒子可以装下这块巧克力。也就是说这种方法实际过程中不能考虑正确。

故而把巧克力按 \(a\) 降序排序,使用 std::multiset 等找到并分配出对应的盒子即可。

实际代码可能有所不同。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
int n,m,i;
struct rec{
    int h,w;
    inline bool operator <(const rec &a)const{
        if(h!=a.h) return h<a.h;
        return w<a.w;
    }
}C[maxn],B[maxn];
multiset<int> s;
multiset<int>::iterator it;
int main(){
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;++i) scanf("%d",&(C+i)->h);
    for(i=1;i<=n;++i) scanf("%d",&(C+i)->w);
    for(i=1;i<=m;++i) scanf("%d",&(B+i)->h);
    for(i=1;i<=m;++i) scanf("%d",&(B+i)->w);
    sort(C+1,C+n+1);sort(B+1,B+m+1);
    while(n){
        while(m){
            if(B[m].h<C[n].h) break;
            s.insert(B[m--].w);
        }
        it=s.lower_bound(C[n--].w);
        if(it==s.end()){
            printf("No\n");
            return 0;
        }
        s.erase(it);
    }
    printf("Yes\n");
    return 0;
} 

Endless Walk

题意

有一个 \(n\) 个点 \(m\) 条边的简单有向图。求从多少个点出发存在一条长为无穷的路径(不一定是简单路径)。\(n,m\le 2\cdot 10^5\)

解法

模板缩点题,在反图上找每个大小大于 \(1\) 的强连通分量到达的点即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
int n,m,i,j,u,v,t,c,tim,top,ans;
int h[maxn];
struct edge{int to,nxt;}E[maxn];
int dfn[maxn],low[maxn],stk[maxn],scc[maxn],cnt[maxn];
bool in[maxn];
set<int> dag[maxn];
queue<int> q;
void tarjan(const int &p){
    int lp,to;
    dfn[p]=low[p]=++tim;
    stk[++top]=p;in[p]=1;
    for(lp=h[p];lp;lp=E[lp].nxt){
        to=E[lp].to;
        if(!dfn[to]){tarjan(to);low[p]=min(low[p],low[to]);}
        else if(in[to]) low[p]=min(low[p],dfn[to]);
    }
    if(dfn[p]==low[p]){
        ++c;
        do{
            to=stk[top--];
            in[to]=0;
            scc[to]=c;
            ++cnt[c];
        }while(to!=p);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    while(m--){
        scanf("%d%d",&u,&v);
        E[++t]={v,h[u]};h[u]=t;
    }
    for(i=1;i<=n;++i){
        if(!dfn[i]) tarjan(i);
        u=scc[i];
        for(j=h[i];j;j=E[j].nxt){
            v=scc[E[j].to];
            if(u==v) continue;
            dag[v].insert(u);
        }
    }
    for(i=1;i<=c;++i){
        if(in[i]) continue;
        if(cnt[i]>1){
            q.push(i);
            while(!q.empty()){
                u=q.front();q.pop();
                ans+=cnt[u];
                for(int p:dag[u]){
                    if(in[p]) continue;
                    in[p]=1;q.push(p);
                }
            }
        }
    }
    printf("%d",ans);
    return 0;
}

Foreign Friends

题意

有一个 \(n\) 个点 \(m\) 条边的无向图。第 \(i\) 个点有一个颜色 \(a_i\),且有 \(l\) 个关键点 \(b_1,b_2,\cdots,b_l\)。求每个点到任意一个异色关键点的最短路。\(n,m,k,l\le 10^5\)

解法

由于每一个关键点只可能有一种颜色,所以可以考虑某种颜色的点到某个点的最短路。由于某个点的颜色只有一种,故而维护上述所有最短路的最小的两个已经可以达成目的。

同时,我们在更新一条最短路时立即将其能到达的所有点的总距离和对应颜色压入堆中,将某个元素从堆中取出再考虑其最短路的更新。同时在更新完某个点的两种最短路后即不用再进行以上操作,由于之后进行更新的最短路和对应颜色都会被前两者之一替代。

由于某个点的一条最短路只有在更新时才会引起对其他点最短路的更新,且此过程最多发生两次,故而时间复杂度是 \(O(n\log m)\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f3f3f3f3f
const int maxn=100010;
int n,m,k,l,i,u,v,w,t;
int a[maxn],h[maxn],fr[maxn],fs[maxn];
ll d,d0[maxn],d1[maxn];
struct edge{int to,ed,nxt;}E[maxn<<1];
struct node{
    ll dis;
    int now,ct;
    inline bool operator <(const node &a)const{return dis>a.dis;}
}T;
priority_queue<node> q;
int main(){
    memset(d0,0x3f,sizeof(d0));
    memset(d1,0x3f,sizeof(d1));
    scanf("%d%d%d%d",&n,&m,&k,&l);
    for(i=1;i<=n;++i) scanf("%d",a+i);
    while(l--){
        scanf("%d",&u);
        q.push((node){0,u,a[u]});
    }
    while(m--){
        scanf("%d%d%d",&u,&v,&w);
        E[++t]={v,w,h[u]};h[u]=t;
        E[++t]={u,w,h[v]};h[v]=t;
    }
    while(!q.empty()){
        T=q.top();q.pop();
        d=T.dis;u=T.now;t=T.ct;
        if(~fr[u]&&fr[u]!=t){
            if(fr[u]){
                fr[u]=-1;
                d1[u]=d;
            }
            else{
                fr[u]=fs[u]=t;
                d0[u]=d;
            }
            for(i=h[u];i;i=E[i].nxt){
                v=E[i].to;w=E[i].ed;
                q.push((node){d+w,v,t});
            }
        }
    }
    for(i=1;i<=n;++i){
        if(d0[i]==INF) d0[i]=-1;
        if(d1[i]==INF) d1[i]=-1;
        if(fs[i]!=a[i]) printf("%lld ",d0[i]);
        else printf("%lld ",d1[i]);
    }
    return 0;
}
posted @ 2022-10-04 13:13  Fran-Cen  阅读(25)  评论(0编辑  收藏  举报