Potyczki Algorytmiczne 2021

Round 1

Oranżada [B]

题意

给定一个长度为 \(n\) 的序列 \(a\),每次操作可以交换相邻两个元素,问至少需要多少次操作才能使得前 \(k\) 个元素两两不同。

\(n\leq 5\times 10^5\)

题解

容易发现,一个数字如果它前面已经出现过了,那么这个数字就没有用了。不妨把有用的数字记为 \(1\),没用的记为 \(0\),那么题意等价于每次交换相邻两个数字,使得 \(01\) 串前 \(k\) 个位置都是 \(1\)。贪心把前 \(k\)\(1\) 放到前面即可。

复杂度 \(O(n)\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=500010;
int a[N];bool vis[N];
int main()
{
    int n,k;scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    long long ans=0;
    for(int i=1,p=1;i<=k;i++)
    {
        while(p<=n && vis[a[p]]) p++;
        if(p>n){puts("-1");return 0;}
        vis[a[p]]=true;
        ans+=p-i;
    }
    printf("%lld\n",ans);
    return 0;
}

Od deski do deski [A]

题意

定义一次操作可以删除序列的一个区间,满足区间长度 \(\geq 2\) 且两端数字相等。定义一个序列是好的,当且仅当可以通过若干次操作把这个序列删空。

问所有长度为 \(n\),所有元素 \(\in[1,m]\) 的序列中有多少个序列是好的。对 \(10^9+7\) 取模。

\(n\leq 3000,m\leq 10^9\)

题解

一直往容斥方面想,然后发现状态没法维护,直接寄了。

考虑换一个方向,考虑怎么判断一个序列是否合法。这个可以从前往后 dp,然后对于每个位置,如果它前面存在一个和它相同的位置 \(i\) 并且 \(dp_{i-1}=1\),那么这个前缀也是合法的。

反过来说,当前位置是否合法只和 \(j\in[1\sim i)\) 中满足 \(dp_{j-1}=1\) 的位置的不同颜色数有关。由于其中不包括 \(dp_i\),所以再额外记一维 \(dp_{i-1}\) 即可。

即用 \(f_{i,j,0/1}\) 表示前 \(i\) 个位置,满足 \(dp_{j-1}=1\)\(a_j\) 本质不同数量有 \(j\) 种,\(dp_{i}=0/1\) 的方案数。转移枚举当前位置与上一个位置的 \(dp\) 值即可。

复杂度 \(O(n^2)\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=3010,mod=1000000007;
int f[N][N][2];
void add(int &x,int y){x=(x+y>=mod?x+y-mod:x+y);}
int main()
{
    int n,m;scanf("%d%d",&n,&m);
    f[1][1][0]=m;
    for(int i=2;i<=n;i++)
        for(int j=1;j<=i;j++)
        {
            add(f[i][j][1],1ll*j*(f[i-1][j][0]+f[i-1][j][1])%mod);
            add(f[i][j][0],(1ll*(m-j+1)*f[i-1][j-1][1]+1ll*(m-j)*f[i-1][j][0])%mod);
        }
    int ans=0;
    for(int i=1;i<=n;i++) add(ans,f[n][i][1]);
    printf("%d\n",ans);
    return 0;
}

Round 2

Pandemia [B]

题意

\(n\) 个球排成一排,一开始有一些球是黑的。现在进行若干轮操作,每次你可以将一个没有被染黑的球染白,然后如果对于所有没有染色的球,如果它在一个黑色球旁边,那么它会被染黑。在所有球都被染色后,最小化黑色球个数。

\(n\leq 10^5\)

题解

可以发现最小化黑色球数目等价于最大化无色球数目。把黑色球之间的区间拿出来,最优方案一定是舍弃一些区间,然后尽可能增加剩下区间中的无色球数目。

枚举两侧的区间是否要舍弃,中间的区间一定是优先舍弃小区间,直接贪心即可。复杂度 \(O(n)\)

代码咕咕咕了。

Poborcy podatkowi [A]

题意

给定一棵树,边有边权(可能为负)。要求在树上选出若干条长度为 \(4\)\(5\) 个点)的边不交路径,使得边权和最大。

\(n\leq 2\times 10^5\)

题解

看到过好几次的题?考虑 dp,设 \(f_{i,j}\) 表示当前以 \(i\) 为根子树内,根所在链长度为 \(j\) 的边权和。考虑子树中 dp 需要让 \(1,3\) 匹配,\(2\) 两两匹配。然而 \(1,3\) 数量可能会很大,这可能会导致 \(O(n)\) 的额外复杂度。考虑将子树 shuffle 一下,根据霍夫丁不等式,此时最优解出现前缀 \(1\)\(3\) 个数差超过 \(\sqrt n\) 的概率是 \(10^{-6}\) 左右,可以近似认为不会发生。

直接 dp 即可。复杂度 \(O(n\sqrt n)\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<vector>
#include<random>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=200010;const ll inf=0x3f3f3f3f3f3f3f3f;
mt19937 Rnd(123);
int nxt[N<<1],to[N<<1],w[N<<1],head[N],cnt;
void chkmax(ll &x,ll y){x=max(x,y);}
void add(int u,int v,int w0){nxt[++cnt]=head[u];to[cnt]=v,w[cnt]=w0;head[u]=cnt;}
ll f[N][4];
void dfs(int u,int p)
{
    int c=0;
    vector<pair<int,int>>son;
    for(int i=head[u];i;i=nxt[i]) if(to[i]!=p) dfs(to[i],u),++c,son.emplace_back(to[i],w[i]);
    shuffle(son.begin(),son.end(),Rnd);
    int s=max(2*(int)sqrt(c)+1,8),sz=0;static ll t1[N][2][4],t2[N][2][4];auto g=t1+s,h=t2+s;
    for(int i=-s;i<=s;i++)
        for(int x=0;x<=3;x++) g[i][0][x]=g[i][1][x]=-inf;
    g[0][0][0]=0;
    for(auto [v,w]:son)
    {
        sz=min(sz+1,s-1);
        for(int j=-sz;j<=sz;j++)
            for(int x=0;x<=1;x++)
                for(int y=0;y<=3;y++)
                    h[j][x][y]=g[j][x][y],g[j][x][y]=-inf;
        for(int j=-sz;j<=sz;j++)
        {
            for(int x=0;x<=3;x++) if(f[v][x]>-inf)
                for(int k=0;k<=1;k++)
                    for(int y=0;y<=3;y++)
                    {
                        ll res=h[j][k][y]+f[v][x]+w;
                        if(x!=3 && !y) chkmax(g[j][k][x+1],res);
                        if(x==3) chkmax(g[j][k][y],res);
                        if(x==2) chkmax(g[j+1][k][y],res);
                        if(x==1) chkmax(g[j][!k][y],res);
                        if(x==0) chkmax(g[j-1][k][y],res),chkmax(g[j][k][y],h[j][k][y]+f[v][x]);
                    }
        }
    }
    for(int i=0;i<=3;i++) f[u][i]=g[0][0][i];
}
int main()
{
    memset(f,0xcf,sizeof(f));
    int n;scanf("%d",&n);
    for(int i=1,x,y,w;i<n;i++)
    {
        scanf("%d%d%d",&x,&y,&w);
        add(x,y,w),add(y,x,w);
    }
    dfs(1,0);
    printf("%lld\n",f[1][0]);
    return 0;
}

Round 3

Mopadulo [B]

题意

\(mod=10^9+7\),给定一个长度为 \(n\) 的序列 \(a\),问把 \(a\) 分成若干区间的方案数,使得每个区间的和对 \(mod\) 取模后结果是偶数。\(n\leq 3\times 10^5\)

题解

考虑 dp。显然有一个 \(O(n^2)\) 的区间 Dp。容易发现,\(s_i-s_j\)\(mod\) 取模后的奇偶性,等于 \(s_i\) 取模后奇偶性异或 \(s_j\) 取模后奇偶性异或 \([s_i<s_j]\)

直接对奇偶性分讨然后用树状数组优化。复杂度 \(O(n\log n)\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=300010,mod=1000000007;
int a[N],b[N],s[N],p[N],n,m;
struct fenwick_pre{
    int a[N];
    void add(int x,int v){for(;x<=m;x+=x&-x) a[x]=(a[x]+v)%mod;}
    int qry(int x){int v=0;for(;x;x-=x&-x) v=(v+a[x])%mod;return v;}
}pre[2];
struct fenwick_suf{
    int a[N];
    void add(int x,int v){for(;x;x-=x&-x) a[x]=(a[x]+v)%mod;}
    int qry(int x){int v=0;for(;x<=m;x+=x&-x) v=(v+a[x])%mod;return v;}
}suf[2];
int f[N];
int main()
{
    int n;scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),s[i]=(s[i-1]+a[i])%mod;
    for(int i=0;i<=n;i++) b[++m]=s[i];
    sort(b+1,b+m+1),m=unique(b+1,b+m+1)-b-1;
    for(int i=0;i<=n;i++) p[i]=lower_bound(b+1,b+m+1,s[i])-b;
    pre[0].add(p[0],1),suf[0].add(p[0],1);
    for(int i=1;i<=n;i++)
    {
        f[i]=(pre[s[i]&1].qry(p[i])+suf[!(s[i]&1)].qry(p[i]+1))%mod;
        pre[s[i]&1].add(p[i],f[i]),suf[s[i]&1].add(p[i],f[i]);
    }
    printf("%d\n",f[n]);
    return 0;
}

Wystawa [A]

题意

给定两个长度为 \(n\) 的序列 \(a,b\),要求构造一个序列 \(c\)\(c_i\) 等于 \(a_i\) 或者 \(b_i\),并且恰好有 \(k\) 个位置等于 \(a_i\),其余位置等于 \(b_i\)。最小化 \(c\) 的最大子段和。

\(n\leq 10^5\)

题解

容易得到一个 \(O(n^2\log x)\) 的做法:首先二分答案 \(X\),然后设 \(f_{i,j}\) 表示前 \(i\) 个位置,有 \(j\) 个位置是 \(A\),前缀最长子段和 \(\leq X\) 情况下的最大后缀和最小值。容易写出转移式子:\(f_{i,j}\rightarrow f_{i+1,j}+B_i,f_{i,j}\rightarrow f_{i+1,j+1}+A_i\)。同时所有数字对 \(0\)\(\max\)

考虑将 \((j,f_{i,j})\) 画出,容易发现第一个转移式子相当于把图像向上平移了 \(B_i\),第二个转移式子等价于找到第一个斜率 \(>A_i-B_i\) 的位置,然后将后面的图像移动 \((1,A_i-B_i)\)。可以证明这个图像是下凸的。

考虑求出差分数组,显然差分数组是单调的,那么第二个转移式子等价于插入到不影响单调性的位置上。接下来对 \(0\)\(\max\) 等价于将一段前缀的差分数组变成 \(0\)(需要特殊处理第一个位置),\(\leq X\) 等价于将一段后缀的函数删去。

最后如果图像中存在 \(k\) 位置就是有解,否则无解。

考虑如何构造。一种方式是直接用可持久化线段树记录前驱,但非常难写。实际上有一种非常巧妙的方式:注意到差分数组的每一个数字都是由一个位置的 \(A_i-B_i\) 贡献的。那么不妨记录这些位置,这样只需要将留到最后的差分数组上的这些位置取出就是构造。

复杂度 \(O(n\log n\log x)\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<set>
#include<vector>
#define fi first
#define se second
using namespace std;
const int N=100010;
typedef long long ll;
int a[N],b[N],n,k;bool ban[N];ll sm,al;
vector<int>ans;set<pair<ll,int>>s;
void out(int x){if(ans.size()<k) ans.push_back(x);};
void add(pair<ll,int> x){s.insert(x),al+=x.fi;};
void del(pair<ll,int> x){s.erase(x),al-=x.fi;};
bool check(ll X)
{
    sm=0,al=0,ans.clear(),s.clear();
    for(int i=1;i<=n;i++)
    {
        sm+=a[i];
        if(!ban[i]) add({b[i]-a[i],i});
        while(sm+al>X)
        {
            if(s.empty()) return false;
            del(*s.rbegin());
        }
        while(sm<0)
        {
            if(s.empty()){sm=0;break;}
            auto p=*s.begin();del(p);
            if(sm+p.fi<0) out(p.se),sm+=p.fi;
            else p.fi+=sm,sm=0,add(p);
        }
    }
    for(auto v:s) out(v.se);
    return ans.size()>=k;
}
int w[N];
int main()
{
    scanf("%d%d",&n,&k),k=n-k;
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) scanf("%d",&b[i]);
    int k0=0,flg=0;for(int i=1;i<=n;i++) if(a[i]>b[i]) ++k0;
    if(k0>k){flg=1;for(int i=1;i<=n;i++) swap(a[i],b[i]);k=n-k;}
    for(int i=1;i<=n;i++) if(a[i]>b[i]) --k,swap(a[i],b[i]),ban[i]=true;
    ll l=0,r=1e15,res=0;
    while(l<=r)
    {
        ll mid=(l+r)>>1;
        if(check(mid)) r=mid-1,res=mid;
        else l=mid+1;
    }
    check(res);
    printf("%lld\n",res);
    for(int i=1;i<=n;i++) w[i]=ban[i];
    for(int v:ans) w[v]=1;
    for(int i=1;i<=n;i++) putchar((w[i]^flg)?'B':'A');
    return 0;
}

Round 4

Skrzyżowania [B]

题意

平面上有 \((n+1)\times (m+1)\) 个街区,被 \(n\) 条横向道路和 \(m\) 条纵向道路分开。每个十字路口有一个红绿灯。红绿灯以不大于 \(8\) 的周期运作,如果为 \(0\) 表示这个时刻其左边的两个街区分别与右边的两个街区相连,否则表示其上方的两个街区分别与下方的两个街区相连。你可以在一个时刻内通过任意相连的街区。

\(q\) 次询问,每次问你在 \(t_i\) 时刻从 \((a_i,b_i)\)\((c_i,d_i)\) 最早什么时刻可以到达。

题解

手模一下会发现大部分情况似乎所有街区都是连通的,除了某一排的红绿灯全部横向连接,或者某一列的红绿灯全部纵向连接。

任意时刻不会同时出现两种情况,所以可以把行列分开算。由于 \(q\) 很大,考虑离线分治,每次枚举中间行的通过时间对 \(k=3\times 5\times 7\times 8=840\) 取模的结果,左右可以分别贪心计算。

复杂度 \(O(nm+k(n+m)\log n+q\log n)\)。非常卡常,过不太去。

考虑换个思路。不妨假设当前处理的是行,那么可以构造一个 \(n\times k\) 的网格,其中相邻列之间的某些边界不能通过,其余都可以通过。每次可以往上或者往右走,最顶上可以继续往上走走到最底下。容易发现,不妨假设一个点往右是 \(0\),往上是 \(1\),找到从这个点开始字典序最小的到达最右端的路径,那么对于一次从这个点开始到 \(r\) 结束的询问,只需要找到路径中第一次到达第 \(r\) 列的时间即可。

而在这道题中这等价于找到反串字典序最大的。所以直接从右端开始 bfs 就可以确定这个路径树,然后再路径树上 dfs 即可。

复杂度 \(O(nm+k(n+m)+q)\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<ctime>
#include<queue>
#define fi first
#define se second
using namespace std;
const int N=15010,M=842,m=840,K=1000010;
vector<vector<vector<int>>>s;
int ans[K],q;
struct sub{
    bool f[N][M],vis[N*M];int ql[K],qr[K],qt[K],n;
    vector<int>Q[N*M];int son[N*M][2];
    int id(int x,int y){return x*m+y;}
    int dep[N];
    void dfs(int u)
    {
        int d=u/m;
        for(int v:Q[u])
            ans[v]=max(ans[v],dep[d]-dep[qr[v]]);
        for(int i=0;i<=1;i++)
        {
            int v=son[u][i];
            if(v==-1) break;
            if(v/m==d) dep[d]++,dfs(v),dep[d]--;
            else dep[v/m]=dep[d],dfs(v);
        }
    }
    void work()
    {
        memset(son,-1,sizeof(son));
        for(int i=1;i<=q;i++) if(ql[i]<qr[i]) Q[id(ql[i],qt[i]%m)].push_back(i);
        queue<int>qu;
        auto push=[&](int u,int p){
            if(vis[u]) return;
            if(p!=-1)
            {
                if(son[p][0]==-1) son[p][0]=u;
                else if(son[p][1]==-1) son[p][1]=u;
                else throw;
            }
            vis[u]=true,qu.push(u);
        };
        for(int i=0;i<m;i++) push(id(n,i),-1);
        while(!qu.empty())
        {
            int u=qu.front(),x=u/m,y=u%m;qu.pop();
            push(id(x,(y-1+m)%m),u);
            if(x && f[x-1][y]) push(id(x-1,y),u);
        }
        for(int i=0;i<m;i++) dep[n]=0,dfs(id(n,i));
        memset(son,-1,sizeof(son));
        for(int i=0;i<=(n+1)*m;i++) Q[i].clear(),vis[i]=0;
        for(int i=1;i<=q;i++) if(ql[i]>qr[i])
            Q[id(ql[i],qt[i]%m)].push_back(i);
        for(int i=0;i<m;i++) push(id(0,i),-1);
        while(!qu.empty())
        {
            int u=qu.front(),x=u/m,y=u%m;qu.pop();
            push(id(x,(y-1+m)%m),u);
            if(x<n && f[x][y]) push(id(x+1,y),u);
        }
        for(int i=0;i<m;i++) dep[0]=0,dfs(id(0,i));
        for(int i=0;i<=(n+1)*m;i++) Q[i].clear(),vis[i]=0;
    }
}F;
int q0[K],q1[K],q2[K],q3[K],q4[K];
int main()
{
    int r,c;scanf("%d%d%d",&r,&c,&q);
    s.resize(r+1);
    for(int i=0;i<=r;i++) s[i].resize(c+1);
    for(int i=0;i<r;i++)
        for(int j=0;j<c;j++)
        {
            char str[10];scanf("%s",str);
            int l=strlen(str);s[i][j].resize(l);
            for(int k=0;k<l;k++) s[i][j][k]=str[k]-'0';
        }
    for(int i=1;i<=q;i++) scanf("%d%d%d%d%d",&q0[i],&q1[i],&q2[i],&q3[i],&q4[i]);
    F.n=r;
    for(int i=0;i<r;i++)
    {
        static bool g[9][9];memset(g,0,sizeof(g));
        for(int j=0;j<c;j++)
        {
            int l=s[i][j].size();
            for(int k=0;k<l;k++) if(!s[i][j][k]) g[l][k]=true;
        }
        for(int j=0;j<m;j++)
        {
            F.f[i][j]=false;
            for(int l=2;l<=8;l++) if(g[l][j%l]){F.f[i][j]=true;break;}
        }
    }
    for(int i=1;i<=q;i++) F.qt[i]=q0[i],F.ql[i]=q1[i],F.qr[i]=q3[i];
    F.work();
    F.n=c;
    for(int i=0;i<c;i++)
    {
        static bool g[9][9];memset(g,0,sizeof(g));
        for(int j=0;j<r;j++)
        {
            int l=s[j][i].size();
            for(int k=0;k<l;k++) if(s[j][i][k]) g[l][k]=true;
        }
        for(int j=0;j<m;j++)
        {
            F.f[i][j]=false;
            for(int l=2;l<=8;l++) if(g[l][j%l]){F.f[i][j]=true;break;}
        }
    }
    for(int i=1;i<=q;i++) F.qt[i]=q0[i],F.ql[i]=q2[i],F.qr[i]=q4[i];
    F.work();
    for(int i=1;i<=q;i++) printf("%d\n",q0[i]+ans[i]);
    return 0;
}

Areny [A]

题意

给定一张有向图,保证每个点至少有 \(1\) 条出边。有一个参数 \(k\),从一个点 \(u\) 开始走,每次随机一条出边走出去,如果走到 \(>k\) 的点就结束操作。对于 \(k\in[1,n]\),问有多少对点 \((u,v)\) 满足 \(u\neq v\) 且从 \(u\) 开始走一定可以在有限步数走到 \(v\)

\(n\leq 2\times 10^5,m\leq 5\times 10^5\)

题解

为了简化,记 \(u\leadsto v\) 表示从 \(u\) 出发一定能到 \(v\)

首先考虑 \(k=n\) 的情况。显然有一个 \(O(n^2)\) 做法是:枚举终点 \(v\),显然 \(v\leadsto v\),如果一个点 \(u\) 所有出边对应点 \(v'\) 都有 \(v'\leadsto v\),那么 \(u\leadsto v\)。显然这样类似拓扑的过程是充要的。

考虑一个性质:如果 \(u\leadsto v,u\leadsto v'\),那么 \(v\leadsto v'\)\(v'\leadsto v\) 至少有一个满足。并且显然 \(u\leadsto v,v\leadsto w\) 可以推出 \(u\leadsto w\)。所以可以将整张图划成若干子图,满足每个子图 \(G'\) 内都存在一个点 \(rt\in G'\),使得 \(\forall v\in G',v\leadsto rt\)

分割子图的过程可以用启发式合并加拓扑在 \(O(m\log n)\) 内解决。

容易发现,如果 \((u,v)\) 在不同子图中,那么一定不存在 \(u\leadsto v\),所以可以对于每个子图分别处理。

这样的一个好处就是:对于每张子图 \(G'\),取 \(rt\) 为根,所有点都存在到根的路径,且去掉 \(rt\) 连出的边后,整个子图是一张 DAG。这样可以把一个点 \(u\) 开始走的路径分成从 \(u\) 开始到 \(rt\) 和从 \(rt\) 出发回到 \(rt\)。只要建出 \(G'\) 的除去 \(rt\) 出边外的支配树,上述过程都可以直接在支配树上解决。复杂度 \(O(m\log n)\)

接下来考虑解决 \(k=1\cdots n\) 的部分。首先可以计算出一个点 \(u\) 到其支配点过程中可能到达的点编号最大值 \(w\),如果 \(w>k\),可以证明 \(u\) 就不存在支配点了(即不存在 \(v\) 使得 \(u\leadsto v\))。但是对于 \(rt\),这样做可能是不正确的:可能存在一个点编号非常大,但实际上在这个点之前存在一个基环上的点是必经点。

考虑直接找出这个环,然后以环上最大编号点作为根。这样就不会存在上述的问题了。

题意简化为:有一个基环内向树森林,对于每个 \(k\),求出有多少 \((x,y)\) 满足 \(x\)\(y\) 最短路径上边权均 \(\leq k\)。考虑找到环上最大的边,那么在这条边断掉之前可以直接套用树的做法,这条边加上后可以看做一棵新的树。用 set
启发式合并即可做到 \(O(n\log^2 n)\),用其他类似的东西可以做到 \(O(n\log n)\)

感觉非常难写。咕咕咕了。

Round 5

Autostrada [B]

题意

\(3\) 条道路,上面有若干辆 \(1\times 1\) 的车,通过一个 \(3\times L\) 的矩阵描述。第 \(i\) 条道路上的车子车速为 \(v_i\)

你现在在最后一格,需要超过所有车。你的车子最快速度是 \(v_0\),并且可以在相邻道路之间任意切换,切换不需要时间但是需要对应位置没有车。你不一定需要在整数时间内切换。现在你希望知道你超过所有车的最小时间。

\(L\leq 2\times 10^5,v_3<v_2<v_1<v_0\leq 140\)

题解

考虑以中间车道的车为基准,假设中间车道的车是不动的,相当于左侧的车在往前,右侧的车在往后。

容易发现,在任何一条车道,当前的车要么全速往前,要么与当前车道保持相对禁止。

考虑可能的策略。首先在中间车道如果前面是空着的一定全速往前,否则可以转到左右侧车道,或者等待左右侧车道的空位。

可以发现,无论在左侧车道还是右侧车道,只要中间车道是空着的,转移到中间车道一定不劣。同时对于左侧车道,还有一个附加的策略是一直跟在最前面的车后面,直到到达中间车道的下一个位置。

代码有点难写,先咕咕咕了。

Desant 2 [B]

题意

给定整数序列 \(a\),有 \(q\) 次询问,每次询问一个区间 \([l,r]\),问从中选出若干不交的长度为 \(k\) 的区间,最大化区间内部 \(a\) 之和。

题解

考虑将 \(a\) 列成一个 \(k\times \frac nk\) 的网格,那么 \(i\)\(i+1\)\(i+k\) 连边相当于网格中向上和向右连边(最顶上的点向下一列最底下的点连边)。

考虑根据 \(k\)\(\sqrt n\) 的关系分讨。如果 \(k<\sqrt n\),那么直接按行分治。否则特别处理第一行的连边,然后按列分治。

复杂度 \(O(n\sqrt n\log n)\)。如果每次都取较大的一边分治,可以做到 \(O(n\sqrt n)\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int N=600010;
typedef long long ll;const ll inf=1e18;
int n,m,q,a[N];ll s[N];
void chkmax(ll &x,ll y){x=max(x,y);}
struct node{
    int x,y;
    node(){x=y=0;}node(int s):x(s%m),y(s/m){}
    node(int x,int y):x(x),y(y){}
};
int id(int x,int y){return x+y*m;}
bool in(node l,node r,node u){return l.x<=u.x && u.x<=r.x && l.y<=u.y && u.y<=r.y;}
ll f[N],g[N];
void upd(node l,node r,node p)
{
    for(int i=l.x;i<=r.x;i++)
        for(int j=l.y;j<=r.y;j++) f[id(i,j)]=g[id(i,j)]=-inf;
    f[id(p.x,p.y)]=g[id(p.x,p.y)]=0;
    for(int j=l.y;j<=r.y;j++)
        for(int i=l.x;i<=r.x;i++)
        {
            int u=id(i,j);
            if(in(l,r,u+1)) chkmax(g[u+1],g[u]);
            if(in(l,r,u+m)) chkmax(g[u+m],g[u]+s[u+m]-s[u]);
        }
    for(int j=r.y;j>=l.y;j--)
        for(int i=r.x;i>=l.x;i--)
        {
            int u=id(i,j);
            if(in(l,r,u-1)) chkmax(f[u-1],f[u]);
            if(in(l,r,u-m)) chkmax(f[u-m],f[u]+s[u]-s[u-m]);
        }
}
int ql[N],qr[N];ll ans[N];
void solve1(node l,node r,vector<int>&Q)
{
    if(l.x>r.x || l.y>r.y || Q.empty()) return;
    // cerr<<l.x<<" "<<l.y<<" "<<r.x<<" "<<r.y<<":";
    // for(int v:Q) cerr<<v<<" ";cerr<<endl;
    vector<int>L,R;
    int p=(l.x+r.x)>>1;node pl={p-1,r.y},pr={p+1,l.y};
    for(int v:Q) if(in(l,pl,ql[v]) && in(l,pl,qr[v])) L.push_back(v);
    else if(in(pr,r,ql[v]) && in(pr,r,qr[v])) R.push_back(v);
    solve1(l,pl,L),solve1(pr,r,R);
    for(int i=l.y;i<=r.y;i++)
    {
        upd(l,r,{p,i});
        for(int v:Q) chkmax(ans[v],f[ql[v]]+g[qr[v]]);
    }
    if(l.x==0 && r.x==m-1)
        for(int i=l.y;i<=r.y;i++)
        {
            upd(l,r,{r.x,i});
            for(int v:Q) chkmax(ans[v],f[ql[v]]+g[qr[v]]);
        }
}
void solve2(node l,node r,vector<int>&Q)
{
    if(l.x>r.x || l.y>r.y || Q.empty()) return;
    vector<int>L,R;
    int p=(l.y+r.y)>>1;node pl={r.x,p-1},pr={l.x,p+1};
    for(int v:Q) if(in(l,pl,ql[v]) && in(l,pl,qr[v])) L.push_back(v);
    else if(in(pr,r,ql[v]) && in(pr,r,qr[v])) R.push_back(v);
    solve2(l,pl,L),solve2(pr,r,R);
    for(int i=l.x;i<=r.x;i++)
    {
        upd(l,r,{i,p});
        for(int v:Q) chkmax(ans[v],f[ql[v]]+g[qr[v]]);
    }
}
int main()
{
    scanf("%d%d%d",&n,&m,&q);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(++n;n%m;++n);
    for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
    vector<int>id;
    for(int i=1;i<=q;i++) scanf("%d%d",&ql[i],&qr[i]),id.push_back(i),--ql[i];
    if(m<=633) solve2({0,0},{m-1,n/m-1},id);
    else solve1({0,0},{m-1,n/m-1},id);
    for(int i=1;i<=q;i++) printf("%lld\n",ans[i]);
    return 0;
}

Fiolki 2 [A]

题意

给定一张 \(n\) 个点的 DAG,保证前 \(k\) 个点没有入度。对于 \(x\in[0,k]\),输出有多少个区间 \([l,r]\),使得从 \([1,k]\) 开始到 \([l,r]\) 内的点结束的不相交路径条数恰好为 \(x\)

\(n\leq 10^5,k\leq 50\)

题解

其实和 这题 很像。

考虑构造一个 \(k\) 维向量,对于前 \(k\) 个点,令第 \(i\) 个点点权 \(f_i\) 是除了第 \(i\) 个位置为 \(1\),其余位置全部为 \(0\) 的向量。对于 \(k+1\sim n\) 的点,令第 \(i\) 个点点权 \(f_i\) 是满足 \(v_j=i\) 的边 \((u_j,v_j)\)\(f_{u,j}x_j\) 之和。

容易证明,最后区间 \([l,r]\) 中极大线性无关组数量就是答案。证明考虑用类似 LGV 引理的思路,要判断是否存在满流只需要判断 \(|\{f_i|i\in[l,r]\}|\) 是否满秩即可,同样要判断流量大小等于矩阵秩的大小,也即极大线性无关组数量。

直接取 \(x_i\) 复杂度是无法接受的。根据经典套路,我们令 \(x_i\) 等于一个 \([0,P)\) 的随机值,然后求出乘积在模 \(P\) 意义下的结果。根据 Schwartza-Zippela 定理,这个随机值导致基变小的概率是 \(\frac 1P\)

这样维护一个广义线性基。注意到这里我们需要时刻维护所有基中编号最大的元素,由于线性基满足拟阵性质,每次插入的向量 \(x\),当 \(x\) 需要减去线性基中某个元素时,如果那个元素对应编号小于 \(x\) 的编号,就交换它们。可以证明,这样总是在线性基中留下编号最大的元素。

这样直接从前往后扫,然后将线性基中元素排序后左端点在 \([a_i,a_{i+1})\) 的部分就是大小为 \(i\) 的线性基。

复杂度 \(O(mk+nk^2)\)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<random>
#include<queue>
#include<vector>
#include<algorithm>
#define fi first
#define se second
using namespace std;
typedef vector<int> vec;
const int N=100010,K=52,mod=1019260817;
int ksm(int a,int b=mod-2)
{
    int r=1;
    for(;b;b>>=1,a=1ll*a*a%mod) if(b&1) r=1ll*r*a%mod;
    return r;
}
mt19937 Rand(1234);
vec f[N];int n,m,k;vector<int>g[N];int deg[N];
vec operator +(const vec &a,const vec &b){vec c(k);for(int i=0;i<k;i++) c[i]=(a[i]+b[i])%mod;return c;}
vec operator -(const vec &a,const vec &b){vec c(k);for(int i=0;i<k;i++) c[i]=(a[i]-b[i]+mod)%mod;return c;}
vec operator *(const vec &a,int v){vec c(k);for(int i=0;i<k;i++) c[i]=1ll*a[i]*v%mod;return c;}
queue<int>q;
pair<int,vec> b[K];
void insert(pair<int,vec> x)
{
    for(int i=k-1;i>=0;i--) if(x.se[i])
    {
        if(!b[i].fi){b[i]=x;break;}
        else
        {
            if(b[i].fi<x.fi) swap(b[i],x);
            int iv=1ll*x.se[i]*ksm(b[i].se[i])%mod;
            x.se=x.se-b[i].se*iv;
        }
    }
}
long long ans[K];
int main()
{
    scanf("%d%d%d",&n,&m,&k);
    for(int i=0,x,y;i<m;i++) scanf("%d%d",&x,&y),--x,--y,g[x].push_back(y),deg[y]++;
    for(int i=0;i<n;i++) f[i].resize(k);
    for(int i=0;i<n;i++) if(!deg[i]) q.push(i);
    for(int i=0;i<k;i++) f[i][i]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int v:g[u])
        {
            f[v]=f[v]+f[u]*(Rand()%(mod-1)+1);
            if(!--deg[v]) q.push(v);
        }
    }
    for(int i=k;i<n;i++)
    {
        insert({i,f[i]});
        static int tmp[K];int tt=0;
        for(int j=0;j<k;j++) if(b[j].fi) tmp[++tt]=b[j].fi;
        tmp[++tt]=k-1,tmp[0]=i;
        sort(tmp+1,tmp+tt+1,greater<int>());
        for(int j=0;j<tt;j++) ans[j]+=tmp[j]-tmp[j+1];
    }
    for(int i=0;i<=k;i++) printf("%lld\n",ans[i]);
    return 0;
}

Zbiory niezależne [A]

Poly 题,爬了。

posted @ 2022-08-16 20:00  Flying2018  阅读(410)  评论(0)    收藏  举报