2024ICPC(济南站)
I
签。
有一个序列,每次你可以选择 恰 \(k\) 个数乘起来,并将这 \(k\) 个数删掉后替换成他们的乘积。
求最终序列最大值的最大可能值对 \(998244353\) 取模的结果。
\(n\le 2\times 10^5,k\le n,0\le V\le 10^9\)。
直接模拟就行,如果前 \(k\) 大有 \(0\) 就不乘。
有个技巧,先把所有非 \(0\) 值拿出来,设有 \(m\) 个。
我们每次消耗 \(k-1\) 个数字,所以只会做 \((m-1)/(k-1)\) 轮,于是只有 \(x=(m-1)\bmod (k-1)\) 个数字乘不上,于是扔掉前 \(x\) 小的就行了,这样非常好写。
C
小博弈。
两个绝顶聪明的人 \(A,B\) 博弈,给定 \([l,r]\) 的整数,令 \(x=l\),一开始 \(A\) 先手,重复进行以下操作:
当前回合的人从这些整数中取出一个 \(x\) 的倍数,取不出来就输了。
令 \(x\gets x+1\),交给另一个人。
求谁会赢。
首先发现偶数不会影响奇数。
其次必定有一个人只能取偶数,一个人只能取奇数,这根据 \(l\) 的奇偶性决定。
我们称一个人正常取当且仅当对于任意时刻,这个人取的数字都是 \(x\)。
这启发我们对 \(l,r\) 的奇偶性讨论:
-
\(l\) 奇 \(r\) 奇,说明 \(A\) 取奇数,且最后一次是 \(A\) 取,\(A\) 正常取就必胜。
-
\(l\) 偶 \(r\) 奇,说明 \(B\) 取奇数,且最后一次是 \(B\) 取,\(B\) 正常取就必胜。
-
\(l\) 奇 \(r\) 偶,说明 \(A\) 取奇数,需要让 \(B\) 在某个时刻提前取不到,只要 \(2l\le r\) 就可以办到。
-
\(l\) 偶 \(r\) 奇,说明 \(B\) 取奇数,要让 \(A\) 在某个时刻提前取不到,只要 \(2(l+1)\le r\) 就可以办到。
void solve() {
cin>>l>>r;
if((l&1)&&(r&1)) puts("Alice");
if(!(l&1)&&(r&1)) puts("Bob");
if((l&1)&&!(r&1)) {
if(l*2<=r) puts("Alice");
else puts("Bob");
}
if(!(l&1)&&!(r&1)) {
if((l+1)*2<=r) puts("Bob");
else puts("Alice");
}
}
G
给定 \(n\) 条线段与 \(n\) 个横坐标,要将它们一一对应,得到的序列为它们的纵坐标,求序列的最大中位数。
\(n\le 10^5,V\le 10^{18}\),需求函数值不超过 \(10^{18}\)。
首先套路二分中位数,考虑check。
将 \(x\) 排序。
将所有线段 \(\ge mid\) 的范围的 \(x\) 坐标找出来并判断它在 \(x\) 序列上的范围。
问题转化成用 \(n\) 个点尽量多的覆盖 \(n\) 个区间。
贪心即可,结论是尽量让当前节点匹配。
\(O(n\log^2 V)\)。
bool check(int L) { tot=0;
FOR(i,1,n) {
if(a[i]*c[1]+b[i]<=a[i]*c[n]+b[i]) {
int l=1,r=n,pos=n+1;
while(l<=r) {
int mid=l+r>>1;
if(a[i]*c[mid]+b[i]>=L) r=mid-1,pos=mid;
else l=mid+1;
}
if(pos<=n) s[++tot]={pos,n};
} else {
int l=1,r=n,pos=0;
while(l<=r) {
int mid=l+r>>1;
if(a[i]*c[mid]+b[i]>=L) l=mid+1,pos=mid;
else r=mid-1;
}
if(pos>=1) s[++tot]={1,pos};
}
}
if(tot<(n+1)/2) return 0;
sort(s+1,s+tot+1,cmp);
int indx=0,nm=0;
priority_queue<int>q;
FOR(i,1,n) {
while(indx+1<=tot&&s[indx+1].l==i) q.push(-s[indx+1].r),++indx;
while(q.size()&&-q.top()<i) q.pop();
if(q.size()) ++nm,q.pop();
}
return nm>=(n+1)/2;
}
B
给定一张简单无向图与排列 \(p\)。
你现在有个 dfs 函数,输出 dfs 序,并且他可以从任何联通块开始执行。
问至少加多少条边才能使得存在一种合法的 dfs 顺序使得输出的 dfs 序 \(=\) 排列 \(p\),输出方案。
\(|V|\le 3\times 10^5,|E|\le 5\times 10^5\)
首先通过将点重编号使得 \(p\) 变成 \(1,2,3,\dots ,n\)。
有一种显然的方法:
按新编号从小到大扫描联通块,如果没走过就走。
若当前点没有未走过的出点直接返回。
设 \(nw\) 表示当前已经输出完了 \(1\sim nw-1\),现在正要寻找 \(nw\),若当前出点有 \(nw\) 直接走。
否则从该点向 \(nw\) 连边。正确性显然。
实现上可以先对出边排序,接着不断扫描出边,如果遇到没走过的点就不断从当前点向 \(nw\) 连边跑。
复杂度 \(O(n\log n)\),瓶颈在于排序。
```plaintext
const int N=3e5+10;
int n,nw,p[N],m,id[N];
bitset<N>vis;
vector<pair<int,int> >pres,Ans;
vector<int>e[N];
void dfs(int x) {
vis[x]=1;
if(x==n) return ;
for(int y:e[x]) {
while(nw!=y&&!vis[y]) {
Ans.eb(p[x],p[nw]);
++nw;dfs(nw-1);
}
if(vis[y]) continue;
++nw;dfs(y);
}
}
main() {
cin>>n>>m;
FOR(i,1,m) {
int u,v;cin>>u>>v;
pres.eb(u,v);
}
FOR(i,1,n) p[i]=read(),id[p[i]]=i;
for(auto q:pres) e[id[q.fr]].eb(id[q.se]),e[id[q.se]].eb(id[q.fr]);
FOR(i,1,n) sort(e[i].begin(),e[i].end());nw=1;
FOR(i,1,n) if(!vis[i]&&nw==i) ++nw,dfs(i);
cout<<Ans.size()<<'\n';
for(auto q:Ans) cout<<q.fr<<" "<<q.se<<"\n";
return 0;
}
D
给你一个串 \(S\),若 \(S_i=1\) 说明 \(i\) 位置放了 \(10^{18}\) 个石子,否则啥都没放。
你可以选择第 \(1\le i\le n-2\) 堆取出若干个石子,并将 \(i+1,i+2\) 两堆互换。
问是否可以拿完所有石子。
\(|S|\le 10^6\)
考虑对后 \(5\) 个进行分讨。
- \(S_n=S_{n-1}=0\) 有解。
- \(S_{n-2}+S_{n-3}+S_{n-4}\ge 2\) 有解。
- \(S_{n-3}=S_{n-1}=1\) 有解。
设 \(check(l,r)\) 表示 \([l,r]\) 是否可以全部为 \(1\)。
- \(S_r=1\),\(check(l,r)=check(l,r-1)\)
- 否则 \(check(l,r)=check(l-2,r-1)\)。
边界是 \(l\le 0 和 l>r\)。
复杂度线性。
(((a[n-2]=='1'||a[n-3]=='1')&&check(n-4,n-4))|
(a[n]=='0'&&a[n-1]=='0')|
(a[n-1]=='1'&&check(n-3,n-3))|
(check(n-3,n-2))|
(check(n-4,n-3)))?puts("Yes"):puts("No");
E
给定一张联通简单无向图,判断图中所有简单环是否长度相等。
\(|V|,|E|\le 5\times 10^5\)。
求出 vdcc,首先判断单个点双。
发现点双只可能是:孤点、两点一线、单环、只存在两个度数 \(>2\) 的点且将这两个点以及相连的边删掉后剩余若干条链且一开始没有这两个点直接相连的边。
单独对于每个点双是好判断的,求出每个边属于的点双编号就做完了。
但一个点可能属于多个点双,暴力做需要集合求交,怎么办?
引理-已知点双求边集
在正常的、vdcc编号从小到大的 tarjan 算法中,若 \(id_x\) 表示 \(x\) 所在点双中编号最大的点双的编号,则一条边 \((u,v)\) 所在的点双编号是 \(\min(id_u,id_v)\)。
复杂度 \(O(n)\)。
值得注意的是,如果你使用手写栈,多测时一定要将栈清空。因为求 vdcc 的判断条件需要 top+1 位置的值。
int n,m,dfn[N],siz[N],low[N],st[N],id[N],deg[N],fa[N],tot,top,tme;
pair<int,int>E[N];
vector<int>vdcc[N],e[N];
vector<pair<int,int> >vd[N];
int Get(int x) {
if(x==fa[x]) return x;
return fa[x]=Get(fa[x]);
}
void Merge(int x,int y) {
siz[Get(x)]+=siz[Get(y)];
fa[Get(y)]=Get(x);
}
void Clear() {
FOR(i,1,tot) vd[i].clear(),vdcc[i].clear();
FOR(i,1,n) e[i].clear(),dfn[i]=low[i]=id[i]=deg[i]=st[i]=0;
tot=top=tme=0;
}
void dfs(int x) {
dfn[st[++top]=x]=low[x]=++tme;
for(int y:e[x]) {
if(!dfn[y]) {
dfs(y);cmin(low[x],low[y]);
if(low[y]>=dfn[x]) { ++tot;
while(st[top+1]!=y) vdcc[tot].eb(st[top]),id[st[top]]=tot,--top;
vdcc[tot].eb(x);id[x]=tot;
}
} else cmin(low[x],dfn[y]);
}
}
void solve() {
Clear();
cin>>n>>m;
FOR(i,1,m) {
int u=read(),v=read();E[i]={u,v};
e[u].eb(v),e[v].eb(u);
}
FOR(i,1,n) if(!dfn[i]) dfs(i);
FOR(i,1,m) {
int u=E[i].fr,v=E[i].se;
vd[min(id[u],id[v])].eb(u,v);
}
vector<int>vec;
FOR(i,1,tot) {
for(int x:vdcc[i]) fa[x]=x,deg[x]=0,siz[x]=1;
for(auto p:vd[i]) {
int u=p.fr,v=p.se;
deg[u]++,deg[v]++;
}
int cnt=0,U=0,V=0;
for(int x:vdcc[i]) {
if(deg[x]>2) {
++cnt;
if(!U) U=x;
else V=x;
}
}
int Sz=deg[U];
if(!cnt) {
if(vdcc[i].size()<=2) continue;
vec.eb(vdcc[i].size());continue;
}
if(cnt>2) {
puts("No");
return ;
}
for(auto p:vd[i]) {
int u=p.fr,v=p.se;
if(u==U||v==U||u==V||v==V) continue;
if(Get(u)!=Get(v)) Merge(u,v);
}
vector<int>ed;
for(auto x:vdcc[i]) {
if(x==U||x==V||Get(x)!=x) continue;
ed.eb(siz[x]);
}
if(ed.size()!=Sz) {
puts("No");
return ;
}
sort(ed.begin(),ed.end());
ed.erase(unique(ed.begin(),ed.end()),ed.end());
if(ed.size()>1) {
puts("No");
return ;
}
int a=(ed[0]+1)*2;
vec.eb(a);
}
sort(vec.begin(),vec.end());
vec.erase(unique(vec.begin(),vec.end()),vec.end());
if(vec.size()>1) {
puts("No");
return ;
}
puts("Yes");
}
J
给出一个二维平面,其中有 \(n\) 个矩形,矩形之间的关系要么嵌套要么相离,一开始所有矩形状态为 \(0\)。
有两种操作:
^ x
,将编号为 \(x\) 的矩形状态取反。
- k
,查询所有外面嵌套了恰好 \(k\) 层的节点中,其内部嵌套的矩形状态或 \(=1\) 的矩形个数。
首先把树建出来,这个建树可以扫描线 + set。
具体地,将每个矩形拆成两个操作,将所有操作按照 \(x\) 排序。
用 set 维护 \(y\) 坐标,将每个矩形对应的区间拆成两个点。
插入一个区间时找到左端点的前驱,如果这个前驱是左端点说明该矩形的父亲就是这个左端点对应的矩形,否则说明该矩形的父亲与前驱的父亲相同。
cin>>n>>q;int tot=0;
FOR(i,1,n) {
int a=read(),b=read(),cy=read(),d=read();
c[++tot]={b,d,a,i};
c[++tot]={b,d,cy,-i};
}
sort(c+1,c+tot+1,cmp);
set<pair<int,int> >seg,S;
seg.insert({0,0});
seg.insert({2e9,0});
FOR(i,1,tot) {
int l=c[i].l,r=c[i].r;
if(c[i].id<0) {
seg.erase({l,-c[i].id});
seg.erase({r,fa[-c[i].id]});
} else {
auto it=prev(seg.lower_bound({l,c[i].id}));
fa[c[i].id]=(it->se);e[fa[c[i].id]].eb(c[i].id);
seg.insert({l,c[i].id});
seg.insert({r,fa[c[i].id]});
}
}
将修改变为从根到该节点的路径 \(+1\) 或 \(-1\),查询变为深度为 \(k\) 的权值不为 \(0\) 的节点数量。
考虑一次路径加 \(1\) 对答案数组的变化,发现等价于将 \(x\) 与其他关键点中的 lca 的最大深度 \(+1\) 到 \(x\) 的深度,将这些下标上的答案 \(+1\)。
删除同理。
根据 dfs 序的性质,只有 dfs 序相邻的两个点的 lca 才可能成为答案。
因此再维护一个 set,关键字是 dfs 序,每次插入/删除节点时查询前驱后继即可。
至于答案数组的维护,发现这个东西是区间加单点查,直接差分树状数组即可。
复杂度 \(O(n\log n)\)。
dep[0]=-1;
dfs(0);
while(q--) {
char s[2];scanf("%s",s);int x=read();
if(s[0]=='^') {
if(S.count({dfn[x],x})) {
S.erase({dfn[x],x});
auto it=S.upper_bound({dfn[x],x});
int La=0;
if(it!=S.end()) {
int c=lca(it->se,x);
if(dep[La]<dep[c]) La=c;
}
if(it!=S.begin()) {
--it;
int c=lca(it->se,x);
if(dep[La]<dep[c]) La=c;
}
add(dep[La]+1,-1),add(dep[x]+1,1);
} else {
auto it=S.upper_bound({dfn[x],x});
int La=0;
if(it!=S.end()) {
int c=lca(it->se,x);
if(dep[La]<dep[c]) La=c;
}
if(it!=S.begin()) {
--it;
int c=lca(it->se,x);
if(dep[La]<dep[c]) La=c;
}
add(dep[La]+1,1),add(dep[x]+1,-1);
S.insert({dfn[x],x});
}
} else {
printf("%d\n",ask(x+1));
}
}