Potyczki Algorytmiczne 2021
Round 1
Oranżada [B]
题意
给定一个长度为 的序列 ,每次操作可以交换相邻两个元素,问至少需要多少次操作才能使得前 个元素两两不同。
。
题解
容易发现,一个数字如果它前面已经出现过了,那么这个数字就没有用了。不妨把有用的数字记为 ,没用的记为 ,那么题意等价于每次交换相邻两个数字,使得 串前 个位置都是 。贪心把前 个 放到前面即可。
复杂度 。
代码
#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]
题意
定义一次操作可以删除序列的一个区间,满足区间长度 且两端数字相等。定义一个序列是好的,当且仅当可以通过若干次操作把这个序列删空。
问所有长度为 ,所有元素 的序列中有多少个序列是好的。对 取模。
。
题解
一直往容斥方面想,然后发现状态没法维护,直接寄了。
考虑换一个方向,考虑怎么判断一个序列是否合法。这个可以从前往后 dp,然后对于每个位置,如果它前面存在一个和它相同的位置 并且 ,那么这个前缀也是合法的。
反过来说,当前位置是否合法只和 中满足 的位置的不同颜色数有关。由于其中不包括 ,所以再额外记一维 即可。
即用 表示前 个位置,满足 的 本质不同数量有 种, 的方案数。转移枚举当前位置与上一个位置的 值即可。
复杂度 。
代码
#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]
题意
有 个球排成一排,一开始有一些球是黑的。现在进行若干轮操作,每次你可以将一个没有被染黑的球染白,然后如果对于所有没有染色的球,如果它在一个黑色球旁边,那么它会被染黑。在所有球都被染色后,最小化黑色球个数。
。
题解
可以发现最小化黑色球数目等价于最大化无色球数目。把黑色球之间的区间拿出来,最优方案一定是舍弃一些区间,然后尽可能增加剩下区间中的无色球数目。
枚举两侧的区间是否要舍弃,中间的区间一定是优先舍弃小区间,直接贪心即可。复杂度 。
代码咕咕咕了。
Poborcy podatkowi [A]
题意
给定一棵树,边有边权(可能为负)。要求在树上选出若干条长度为 ( 个点)的边不交路径,使得边权和最大。
。
题解
看到过好几次的题?考虑 dp,设 表示当前以 为根子树内,根所在链长度为 的边权和。考虑子树中 dp 需要让 匹配, 两两匹配。然而 数量可能会很大,这可能会导致 的额外复杂度。考虑将子树 shuffle 一下,根据霍夫丁不等式,此时最优解出现前缀 与 个数差超过 的概率是 左右,可以近似认为不会发生。
直接 dp 即可。复杂度 。
代码
#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]
题意
令 ,给定一个长度为 的序列 ,问把 分成若干区间的方案数,使得每个区间的和对 取模后结果是偶数。。
题解
考虑 dp。显然有一个 的区间 Dp。容易发现, 对 取模后的奇偶性,等于 取模后奇偶性异或 取模后奇偶性异或 。
直接对奇偶性分讨然后用树状数组优化。复杂度 。
代码
#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]
题意
给定两个长度为 的序列 ,要求构造一个序列 , 等于 或者 ,并且恰好有 个位置等于 ,其余位置等于 。最小化 的最大子段和。
。
题解
容易得到一个 的做法:首先二分答案 ,然后设 表示前 个位置,有 个位置是 ,前缀最长子段和 情况下的最大后缀和最小值。容易写出转移式子:。同时所有数字对 取 。
考虑将 画出,容易发现第一个转移式子相当于把图像向上平移了 ,第二个转移式子等价于找到第一个斜率 的位置,然后将后面的图像移动 。可以证明这个图像是下凸的。
考虑求出差分数组,显然差分数组是单调的,那么第二个转移式子等价于插入到不影响单调性的位置上。接下来对 取 等价于将一段前缀的差分数组变成 (需要特殊处理第一个位置), 等价于将一段后缀的函数删去。
最后如果图像中存在 位置就是有解,否则无解。
考虑如何构造。一种方式是直接用可持久化线段树记录前驱,但非常难写。实际上有一种非常巧妙的方式:注意到差分数组的每一个数字都是由一个位置的 贡献的。那么不妨记录这些位置,这样只需要将留到最后的差分数组上的这些位置取出就是构造。
复杂度 。
代码
#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]
题意
平面上有 个街区,被 条横向道路和 条纵向道路分开。每个十字路口有一个红绿灯。红绿灯以不大于 的周期运作,如果为 表示这个时刻其左边的两个街区分别与右边的两个街区相连,否则表示其上方的两个街区分别与下方的两个街区相连。你可以在一个时刻内通过任意相连的街区。
次询问,每次问你在 时刻从 到 最早什么时刻可以到达。
题解
手模一下会发现大部分情况似乎所有街区都是连通的,除了某一排的红绿灯全部横向连接,或者某一列的红绿灯全部纵向连接。
任意时刻不会同时出现两种情况,所以可以把行列分开算。由于 很大,考虑离线分治,每次枚举中间行的通过时间对 取模的结果,左右可以分别贪心计算。
复杂度 。非常卡常,过不太去。
考虑换个思路。不妨假设当前处理的是行,那么可以构造一个 的网格,其中相邻列之间的某些边界不能通过,其余都可以通过。每次可以往上或者往右走,最顶上可以继续往上走走到最底下。容易发现,不妨假设一个点往右是 ,往上是 ,找到从这个点开始字典序最小的到达最右端的路径,那么对于一次从这个点开始到 结束的询问,只需要找到路径中第一次到达第 列的时间即可。
而在这道题中这等价于找到反串字典序最大的。所以直接从右端开始 bfs 就可以确定这个路径树,然后再路径树上 dfs 即可。
复杂度 。
代码
#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]
题意
给定一张有向图,保证每个点至少有 条出边。有一个参数 ,从一个点 开始走,每次随机一条出边走出去,如果走到 的点就结束操作。对于 ,问有多少对点 满足 且从 开始走一定可以在有限步数走到 。
。
题解
为了简化,记 表示从 出发一定能到 。
首先考虑 的情况。显然有一个 做法是:枚举终点 ,显然 ,如果一个点 所有出边对应点 都有 ,那么 。显然这样类似拓扑的过程是充要的。
考虑一个性质:如果 ,那么 和 至少有一个满足。并且显然 可以推出 。所以可以将整张图划成若干子图,满足每个子图 内都存在一个点 ,使得 。
分割子图的过程可以用启发式合并加拓扑在 内解决。
容易发现,如果 在不同子图中,那么一定不存在 ,所以可以对于每个子图分别处理。
这样的一个好处就是:对于每张子图 ,取 为根,所有点都存在到根的路径,且去掉 连出的边后,整个子图是一张 DAG。这样可以把一个点 开始走的路径分成从 开始到 和从 出发回到 。只要建出 的除去 出边外的支配树,上述过程都可以直接在支配树上解决。复杂度 。
接下来考虑解决 的部分。首先可以计算出一个点 到其支配点过程中可能到达的点编号最大值 ,如果 ,可以证明 就不存在支配点了(即不存在 使得 )。但是对于 ,这样做可能是不正确的:可能存在一个点编号非常大,但实际上在这个点之前存在一个基环上的点是必经点。
考虑直接找出这个环,然后以环上最大编号点作为根。这样就不会存在上述的问题了。
题意简化为:有一个基环内向树森林,对于每个 ,求出有多少 满足 到 最短路径上边权均 。考虑找到环上最大的边,那么在这条边断掉之前可以直接套用树的做法,这条边加上后可以看做一棵新的树。用 set
启发式合并即可做到 ,用其他类似的东西可以做到 。
感觉非常难写。咕咕咕了。
Round 5
Autostrada [B]
题意
有 条道路,上面有若干辆 的车,通过一个 的矩阵描述。第 条道路上的车子车速为 。
你现在在最后一格,需要超过所有车。你的车子最快速度是 ,并且可以在相邻道路之间任意切换,切换不需要时间但是需要对应位置没有车。你不一定需要在整数时间内切换。现在你希望知道你超过所有车的最小时间。
。
题解
考虑以中间车道的车为基准,假设中间车道的车是不动的,相当于左侧的车在往前,右侧的车在往后。
容易发现,在任何一条车道,当前的车要么全速往前,要么与当前车道保持相对禁止。
考虑可能的策略。首先在中间车道如果前面是空着的一定全速往前,否则可以转到左右侧车道,或者等待左右侧车道的空位。
可以发现,无论在左侧车道还是右侧车道,只要中间车道是空着的,转移到中间车道一定不劣。同时对于左侧车道,还有一个附加的策略是一直跟在最前面的车后面,直到到达中间车道的下一个位置。
代码有点难写,先咕咕咕了。
Desant 2 [B]
题意
给定整数序列 ,有 次询问,每次询问一个区间 ,问从中选出若干不交的长度为 的区间,最大化区间内部 之和。
题解
考虑将 列成一个 的网格,那么 向 和 连边相当于网格中向上和向右连边(最顶上的点向下一列最底下的点连边)。
考虑根据 与 的关系分讨。如果 ,那么直接按行分治。否则特别处理第一行的连边,然后按列分治。
复杂度 。如果每次都取较大的一边分治,可以做到 。
代码
#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]
题意
给定一张 个点的 DAG,保证前 个点没有入度。对于 ,输出有多少个区间 ,使得从 开始到 内的点结束的不相交路径条数恰好为 。
。
题解
其实和 这题 很像。
考虑构造一个 维向量,对于前 个点,令第 个点点权 是除了第 个位置为 ,其余位置全部为 的向量。对于 的点,令第 个点点权 是满足 的边 的 之和。
容易证明,最后区间 中极大线性无关组数量就是答案。证明考虑用类似 LGV 引理的思路,要判断是否存在满流只需要判断 是否满秩即可,同样要判断流量大小等于矩阵秩的大小,也即极大线性无关组数量。
直接取 复杂度是无法接受的。根据经典套路,我们令 等于一个 的随机值,然后求出乘积在模 意义下的结果。根据 Schwartza-Zippela 定理,这个随机值导致基变小的概率是 。
这样维护一个广义线性基。注意到这里我们需要时刻维护所有基中编号最大的元素,由于线性基满足拟阵性质,每次插入的向量 ,当 需要减去线性基中某个元素时,如果那个元素对应编号小于 的编号,就交换它们。可以证明,这样总是在线性基中留下编号最大的元素。
这样直接从前往后扫,然后将线性基中元素排序后左端点在 的部分就是大小为 的线性基。
复杂度 。
代码
#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 题,爬了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话