Preface
打的一坨,直接被 Div.2 学弟吊起来打
这场主要是中期的 Easy~mid 写的太慢,导致中后期题没时间写
同时封榜后的决策也有点问题,没有全队 All-in 一个题而是让徐神去写当时 1/27 的 K,虽然可能徐神来想 H 我们也出不来但感觉还是跟榜适合我们队的 level
赛后发现 H 反着填右括号就是很典的 Flow 了,B 根据学长说是个很典的 Min_25 筛,这下被干烂了
A. World Cup
祁神开场写的,我题目都没看,好像是个手玩打表题
#include<bits/stdc++.h>
using namespace std;
void solve(){
vector<int> A(32);
for (int i=0; i<32; ++i) cin >> A[i];
int val = A[0];
sort(A.begin(), A.end());
int pos = lower_bound(A.begin(), A.end(), val) - A.begin();
if (pos >= 31) cout << "1\n";
else if (pos >= 27) cout << "2\n";
else if (pos >= 13) cout << "4\n";
else if (pos >= 6) cout << "8\n";
else if (pos >= 2) cout << "16\n";
else cout << "32\n";
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int t; cin >> t; while (t--) solve();
return 0;
}
B. Graph
首先要注意到,题目中的要求等价于对于所有 \(d\in[1,n]\),所有 \(d\) 的倍数对应的点形成的导出子图连通
从大到小考虑每个 \(d\),并将所有 \(d\) 的倍数对应的点重编号为 \(1\sim m\),考虑将这些点连通
由于要求用最少的边因此一定是连成生成树,生成树计数首先考虑 prufer 序列
假设此时图中共有 \(k\) 个连通块,每个连通块大小为 \(x_1,x_2,\dots,x_k\),则连通它们的方案数为 \((\sum x_i)^{k-2}\times \prod x_i\)
因此现在的问题在于计算连通块的数量(即之前有多少个点已经连通)
推一下会发现重标号的点编号如果是合数则之前一定连通了,同时对于 \(\le \frac{m}{2}\) 的素数 \(p\),它之前一定通过 \(2p\) 与 \(2\) 连通了
因此我们只要知道 \([\frac{m}{2}+1,m]\) 中的素数个数,它们都是独立的孤点;同时还有 \(1\) 作为孤点;剩下的所有点在一个连通块中;此时很容易计算方案数
不难发现最后计算答案可以除法分块,而求区间素数个数可以用 Min_25 筛解决,复杂度 \(O(\frac{n^{\frac{3}{4}}}{\log n})\)
#include<cstdio>
#include<iostream>
#include<cmath>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int mod=998244353;
namespace Min25_Sieve
{
const int N=1e6+5;
int n,lim,pri[N],cnt,vis[N],id1[N],id2[N],g[N],w[N],idx;
inline int ID(CI x)
{
return x<=lim?id1[x]:id2[n/x];
}
inline void init(CI nn)
{
n=nn; lim=(int)sqrt(n);
for (RI i=2;i<=lim;++i)
{
if (!vis[i]) pri[++cnt]=i;
for (RI j=1;j<=cnt&&i*pri[j]<=lim;++j)
if (vis[i*pri[j]]=1,i%pri[j]==0) break;
}
for (RI l=1,r;l<=n;l=r+1)
{
r=n/(n/l); w[++idx]=n/l;
if (n/l<=lim) id1[n/l]=idx;
else id2[n/(n/l)]=idx;
g[idx]=w[idx]-1;
}
for (RI j=1;j<=cnt;++j)
for (RI i=1;i<=idx;++i)
{
if (pri[j]*pri[j]>w[i]) break;
g[i]-=g[ID(w[i]/pri[j])]-(j-1);
}
}
inline int work(CI n)
{
return n<=1?0:g[ID(n)];
}
};
using namespace Min25_Sieve;
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline int solve(CI m)
{
if (m<=2) return 1;
if (m==3) return 3;
int tot=work(m)-work(m/2);
return quick_pow(m%mod,tot)*(m%mod-(tot+1)%mod+mod)%mod;
}
signed main()
{
int n; scanf("%lld",&n);
init(n); int ans=1;
for (RI l=1,r;l<=n;l=r+1)
r=n/(n/l),(ans*=quick_pow(solve(n/l),r-l+1))%=mod;
return printf("%lld",ans),0;
}
C. Permutation Counting 4
这题感觉真没那么简单吧,不知道为啥比赛的时候被过穿了,最后还得靠徐神发力
考虑定义一个 \(n\times n\) 的行列式,其中第 \(i\) 行第 \([l_i,r_i]\) 列为 \(1\),其余位置为 \(0\)
考虑对于原题方案数的计数,比如我们要确定第 \(i\) 个位置填什么,其方案数等价于将第 \(i\) 行所有 \(1\) 的位置删去,并删去与其同行同列的元素后的子行列式方案数之和
不难发现这个式子和行列式的定义只差了个符号,而在模意义下 \((-1)^{i+j}\) 的系数可以视为加法,因此原题的方案数的奇偶性等价于行列式的值的奇偶性
由于这个行列式有性质,可以用一些数据结构来优化高斯消元求解行列式的过程,复杂度 \(O(n\log^2 n)\)
#include <bits/stdc++.h>
int work() {
int n; std::cin >> n;
static std::set<int> hkr[1000001];
for(int i = 1; i <= n; ++i) hkr[i].clear();
int flag = true;
for(int i = 1, l, r; i <= n; ++i) {
std::cin >> l >> r;
// std::cerr << "l, r = " << l << ", " << r << char(10);
if(hkr[l].find(r) != hkr[l].end()) flag = false;
hkr[l].insert(r);
}
if(!flag) return 0;
for(int i = 1; i <= n; ++i) {
if(hkr[i].empty()) return 0;
auto front = hkr[i].begin(); int val = *front; hkr[i].erase(front);
if(val == n) continue;
#define nxt hkr[val + 1]
if(nxt.size() < hkr[i].size()) std::swap(hkr[i], nxt);
for(auto r: hkr[i]) {
if(nxt.find(r) == nxt.end()) nxt.insert(r);
else return 0;
}
#undef nxt
}
return 1;
}
int main() {
std::ios::sync_with_stdio(false);
int t; std::cin >> t; while(t--) std::cout << work() << char(10);
return 0;
}
F. Make Max
思路很简单,考虑从小到大将每个同色段的值进行变化
对于当前 \([l,r]\) 的同色段,求出其左右两侧的值 \(L,R\),手玩一下会发现将其变为较小的一方一定最优
用并查集维护同色段,模拟整个过程即可,注意当 \(L=R\) 时要特别处理一下
后面发现好像直接单调栈求一下两边第一个大于该位置的元素然后递推就行,怪不得比赛的时候这题过得这么快
#include<cstdio>
#include<iostream>
#include<set>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005,INF=2e9;
int t,n,a[N],rst[N],L[N],R[N]; set <int> pos[N];
inline int getL(CI x)
{
return L[x]==x?x:L[x]=getL(L[x]);
}
inline int getR(CI x)
{
return R[x]==x?x:R[x]=getR(R[x]);
}
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n); long long ans=0;
for (RI i=1;i<=n;++i) scanf("%d",&a[i]),rst[i]=a[i];
sort(rst+1,rst+n+1); int m=unique(rst+1,rst+n+1)-rst-1;
for (RI i=1;i<=n;++i) a[i]=lower_bound(rst+1,rst+m+1,a[i])-rst;
for (RI i=0;i<=n+1;++i) L[i]=R[i]=i;
for (RI i=1;i+1<=n;++i) if (a[i]==a[i+1]) L[i+1]=getL(i);
for (RI i=n-1;i>=1;--i) if (a[i]==a[i+1]) R[i]=getR(i+1);
for (RI i=1;i<=n;++i) if (getL(i)==i) pos[a[i]].insert(i);
for (RI i=1;i<=m;++i)
{
for (auto x:pos[i])
{
int l=getL(x),r=getR(x),vl=INF,vr=INF;
if (l-1>=1) vl=a[l-1];
if (r+1<=n) vr=a[r+1];
//printf("x = %d\n",x);
//printf("l = %d; r = %d\n",l,r);
//printf("vl = %d; vr = %d\n",vl,vr);
if (vl==vr)
{
if (vl==INF) continue;
R[l-1]=getR(l); L[l]=getL(l-1);
L[r+1]=getL(r); R[r]=getR(r+1);
ans+=r-l+1; pos[vl].erase(r+1);
} else if (vl<vr)
{
R[l-1]=getR(l); L[l]=getL(l-1);
ans+=r-l+1;
} else
{
L[r+1]=getL(r); R[r]=getR(r+1);
ans+=r-l+1;
}
}
pos[i].clear();
}
printf("%lld\n",ans);
}
return 0;
}
G. The Median of the Median of the Median
刚开始犯病了一直想着直接算答案,而没有想到中位数的经典 trick 二分
考虑直接二分答案 \(x\),不妨把 \(\le x\) 的位置都置为 \(1\),\(>x\) 的位置置为 \(-1\)
此时求 \(b_{l,r}\) 的值就只用维护前缀和即可,此时得到每个 \(b_{l,r}\) 和 \(x\) 的大小关系
同理,求 \(c_{l,r}\) 时就等价于一个二维前缀和的过程,此时得到每个 \(c_{l,r}\) 和 \(x\) 的大小关系
最后再求 \(c_{l,r}\) 的中位数即可得到检验 \(x\) 的方法,总复杂度 \(O(n^2\log n)\)
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=2005;
int n,r[N],a[N],pfx[N],sum[N][N];
inline bool check(CI x)
{
for (RI i=1;i<=n;++i) a[i]=(r[i]<=x);
for (RI i=1;i<=n;++i) pfx[i]=pfx[i-1]+a[i];
for (RI i=1;i<=n;++i) for (RI j=i;j<=n;++j)
{
int ones=pfx[j]-pfx[i-1];
sum[i][j]=(ones*2>=j-i+1);
}
for (RI i=n;i>=1;--i) for (RI j=1;j<=n;++j)
sum[i][j]+=sum[i+1][j]+sum[i][j-1]-sum[i+1][j-1];
int cnt=0;
for (RI i=1;i<=n;++i) for (RI j=i;j<=n;++j)
if (sum[i][j]*2>=(j-i+1)*(j-i+2)/2) ++cnt;
return cnt*2>=n*(n+1)/2;
}
int main()
{
scanf("%d",&n);
for (RI i=1;i<=n;++i) scanf("%d",&r[i]);
int l=1,r=1e9,mid,ret; while (l<=r)
if (check(mid=l+r>>1)) ret=mid,r=mid-1; else l=mid+1;
return printf("%d",ret),0;
}
H. Rainbow Bracket Sequence
直接考虑填左括号的话就不太容易,比赛的时候也想了发现需要上下界费用流,但由于我根本不会这玩意就没继续想,赛后看题解发现真有这种做法
不过其实我们有本质更简单的做法,即先假设所有位置都填了左括号,此时需要选出 \(n\) 个位置将其变为右括号
用经典 trick,合法括号序列的充要条件为:
- 后 \(2i-1\) 个位置中至少要有 \(n\) 个右括号;
- 最后 \(2n\) 个位置中恰好要有 \(n\) 个右括号;
同时由于我们预设了每个位置都填左括号,则下界的要求初始时一定是满足的(特判掉显然无解的情况后)
此时每个颜色就有了一个将左括号转为右括号数的上界,此时就可以用传统的费用流模型来处理了
将每个颜色拆出一个虚点,并求出每个颜色 \(j\) 最多只能转化的右括号个数 \(lim_j\)
- \(S\) 向 \(2n\) 连 \((n,0)\) 的边;
- 点 \(i+1\) 向点 \(i\) 连 \((\lfloor\frac{i}{2}\rfloor,0)\) 的边;
- 点 \(i\) 向点 \(n+c_i\) 连 \((1,v_i)\) 的边;
- 点 \(n+j\) 向 \(T\) 连 \((lim_j,0)\) 的边;
最后用 \(\sum v_i\) 减去该图的最小费用最大流即可
#include<cstdio>
#include<iostream>
#include<cstring>
#include<queue>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=205,INF=1e18;
int T,n,m,l[N],c[N],v[N];
namespace MinCost_MaxFlow
{
const int NN=405,MM=5e6;
struct edge
{
int to,nxt,v,c;
}e[MM<<1]; int head[NN],cur[NN],s,t,cnt=1,dis[NN]; bool vis[NN];
inline void addedge(CI x,CI y,CI v,CI c)
{
//printf("%d %d %d %d\n",x,y,v,c);
e[++cnt]=(edge){y,head[x],v,c}; head[x]=cnt;
e[++cnt]=(edge){x,head[y],0,-c}; head[y]=cnt;
}
#define to e[i].to
inline bool SPFA(CI s,CI t)
{
RI i; for (i=0;i<=t;++i) dis[i]=INF; dis[s]=0;
queue <int> q=queue <int>(); q.push(s);
while (!q.empty())
{
int now=q.front(); q.pop(); vis[now]=0;
for (i=head[now];i;i=e[i].nxt)
if (e[i].v&&dis[now]+e[i].c<dis[to])
if (dis[to]=dis[now]+e[i].c,!vis[to]) q.push(to),vis[to]=1;
}
return dis[t]!=INF;
}
inline int DFS(CI now,CI tar,int tmp,int& flow,int& cost)
{
if (now==tar) return flow+=tmp,tmp;
int ret=0; vis[now]=1;
for (RI& i=cur[now];i&&dis;i=e[i].nxt)
if (!vis[to]&&e[i].v&&dis[to]==dis[now]+e[i].c)
{
int dist=DFS(to,tar,min(tmp-ret,e[i].v),flow,cost);
ret+=dist; e[i].v-=dist; e[i^1].v+=dist; cost+=dist*e[i].c;
if (ret==tmp) break;
}
vis[now]=0; return ret;
}
#undef to
inline void Dinic(CI s,CI t,int& flow,int& cost)
{
flow=cost=0; while (SPFA(s,t)) memcpy(cur,head,(t+1)*sizeof(int)),DFS(s,t,INF,flow,cost);
}
inline void clear(void)
{
memset(head,0,(t+1)*sizeof(int)); cnt=1;
}
};
using namespace MinCost_MaxFlow;
signed main()
{
for (scanf("%lld",&T);T;--T)
{
scanf("%lld%lld",&n,&m); n<<=1; int sum=0;
for (RI i=1;i<=m;++i) scanf("%lld",&l[i]),l[i]=-l[i];
for (RI i=1;i<=n;++i) scanf("%lld",&c[i]),++l[c[i]];
for (RI i=1;i<=n;++i) scanf("%lld",&v[i]),sum+=v[i];
s=n+m+1; t=s+1; addedge(s,n,n/2,0);
for (RI i=n;i>1;--i) addedge(i,i-1,(i-1)/2,0);
for (RI i=1;i<=n;++i) addedge(i,n+c[i],1,v[i]);
bool flag=1; for (RI i=1;i<=m;++i)
{
if (l[i]<0) { flag=0; break; }
addedge(n+i,t,l[i],0);
}
if (flag)
{
int flow,cost; Dinic(s,t,flow,cost);
if (flow==n/2) printf("%lld\n",sum-cost); else puts("-1");
} else puts("-1");
clear();
}
return 0;
}
L. Bull Farm
很有意思的一个题,基本纯靠观察
考虑将空格所在的位置看作点,边用来刻画空格的变换方式,此时询问等价于有向图上两点是否可达
首先将询问离线,每次多出一种操作后会在图中加上一些边,因此考虑每个操作的贡献:
- 若该操作为排列,则形成了若干个置换,此时相当于在图中加入 \(n\) 条双向边;
- 若该操作形成的 \(i\to p_i\) 结构为链套环,此时手玩一下会发现多出来两条有向边 \(x\to z,y\to z\);
动态维护有向图的连通性可以考虑 bitset
,加入一条 \(x\to y\) 的边时,我们枚举所有可以到达 \(x\) 的点 \(i\),将其对于的 bitset
或上 \(y\) 的 bitset
即可
加入一条有向边的复杂度为 \(\frac{n^2}{\omega}\),对于第二种情况还好;对于第一种情况加的边数就会爆掉
不过我们注意到连通性问题其实加入一个生成树就足以将图连通了,因此我们用并查集维护下所有无向边构成的生成树,只有生成树上的点才转化为单向边加入 bitset
中
总复杂度 \(O((n+l)\times \frac{n^2}{\omega})\),手写 bitset
后跑得飞快
#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#include<cstring>
#include<array>
#define RI register int
#define CI const int&
using namespace std;
typedef unsigned long long u64;
const int N=2005,M=1e6+5;
int t,n,l,q,trs[N][N],to[N],fa[N],ans[M]; vector <array <int,3>> ques[N]; char s[M];
const int L=(N>>6)+5;
struct Bitset
{
u64 a[L];
inline void init(void)
{
for (RI i=n>>6;i>=0;--i) a[i]=0;
}
inline void set(CI x)
{
a[x>>6]|=(1ull<<(x&63));
}
inline int test(CI x)
{
return (a[x>>6]>>(x&63))&1ull;
}
inline void OR(const Bitset& ots)
{
for (RI i=n>>6;i>=0;--i) a[i]|=ots.a[i];
}
}F[N];
inline int getfa(CI x)
{
return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d%d%d",&n,&l,&q);
auto decode=[&](const char& a,const char& b)
{
return (a-48)*50+(b-48);
};
for (RI i=1;i<=l;++i)
{
scanf("%s",s);
for (RI j=0;j<n;++j)
trs[i][j+1]=decode(s[j<<1],s[j<<1|1]);
}
for (RI i=0;i<=l;++i) ques[i].clear();
for (RI i=1;i<=q;++i)
{
scanf("%s",s);
int a=decode(s[0],s[1]),b=decode(s[2],s[3]),c=decode(s[4],s[5]);
ques[c].push_back({a,b,i});
}
for (RI i=1;i<=n;++i) F[i].init(),F[i].set(i);
for (RI i=1;i<=n;++i) fa[i]=i;
for (RI i=0;i<=l;++i)
{
if (i!=0)
{
for (RI j=1;j<=n;++j) to[j]=trs[i][j];
static int indeg[N];
for (RI j=1;j<=n;++j) indeg[j]=0;
for (RI j=1;j<=n;++j) ++indeg[to[j]];
int T,Z,cntT=0,cntZ=0,cntO=0;
for (RI j=1;j<=n;++j)
if (indeg[j]==2) ++cntT,T=j;
else if (indeg[j]==0) ++cntZ,Z=j;
else if (indeg[j]==1) ++cntO;
auto link=[&](CI x,CI y)
{
for (RI i=1;i<=n;++i) if (F[i].test(x)) F[i].OR(F[y]);
};
if (cntO==n)
{
for (RI j=1;j<=n;++j)
{
int x=getfa(j),y=getfa(to[j]);
if (x!=y) fa[x]=y,link(x,y),link(y,x);
}
} else if (cntT==1&&cntZ==1&&cntO==n-2)
{
int x=-1,y=-1;
for (RI j=1;j<=n;++j)
if (to[j]==T)
{
if (x==-1) x=j;
else if (y==-1) y=j;
}
link(x,Z); link(y,Z);
}
}
for (auto [a,b,id]:ques[i]) ans[id]=F[a].test(b);
}
for (RI i=1;i<=q;++i) putchar(ans[i]+'0'); putchar('\n');
}
return 0;
}
M. Find the Easiest Problem
纯签,徐神开场直接在 PTA 的 IDE 上写的,因此代码就没存
Postscript
明天就是最后一场网络赛了,能不能别犯病了来点 Highlight
标签:CI,const,Contest,int,Asia,ICPC,include,RI,define From: https://www.cnblogs.com/cjjsb/p/18427326