The 2021 ICPC Asia Macau Regional Contest
Preface
这场是上周六和队里VP的,因为中间其它比赛很多所以就没补题了把过了的题写一下
这场纯被徐神带飞,后期发现FFT精度问题提出了神之一手,然后又轻松写意地秒了道广义SAM,徐神真是太强辣
A. So I'll Max Out My Constructive Algorithm Skills
签到题,我们随便找一条路径,如果它合法就直接输出;否则把它reverse
后输出即可
#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=70;
int t,n,h[N][N],s[N*N],cnt;
int main()
{
//freopen("A.in","r",stdin); freopen("A.out","w",stdout);
for (scanf("%d",&t);t;--t)
{
RI i,j; for (scanf("%d",&n),i=1;i<=n;++i)
for (j=1;j<=n;++j) scanf("%d",&h[i][j]);
for (cnt=0,i=1;i<=n;++i)
if (i&1) for (j=1;j<=n;++j) s[++cnt]=h[i][j];
else for (j=n;j>=1;--j) s[++cnt]=h[i][j];
int l=0,g=0; for (i=1;i<cnt;++i)
l+=s[i]<s[i+1],g+=s[i]>s[i+1];
if (l>g) reverse(s+1,s+cnt+1);
for (i=1;i<=cnt;++i) printf("%d%c",s[i]," \n"[i==cnt]);
}
return 0;
}
B. the Matching System
题目都没看,一眼不会做
C. Laser Trap
题目都没看,祁神开场秒了
要让\((0,0)\)在凸包外面,相当于要保留一些点,使得它们的极角范围\(<\pi\),那么直接极角排序后二分找一下即可
#include<bits/stdc++.h>
using namespace std;
#define int long long
struct Pt{
int x, y;
int cross(Pt b)const{return x*b.y-y*b.x;}
int quad(){
if (x>0 && y>=0) return 1; if (x<=0 && y>0) return 2;
if (x<0 && y<=0) return 3; if (x>=0 && y<0) return 4;
}
};
struct Node{
int qd;
Pt v;
bool operator<(const Node &b)const{return qd!=b.qd ? qd<b.qd : v.cross(b.v)>0;}
};
int t, n;
signed main(){
ios::sync_with_stdio(false); cin.tie(0);
cin >> t;
while (t--){
cin >> n;
vector<Node> vec;
for (int i=0; i<n; ++i){
Pt p;
cin >> p.x >> p.y;
int qd = p.quad();
vec.push_back(Node{qd, p});
vec.push_back(Node{qd+4, p});
}
sort(vec.begin(), vec.end());
int ans = n;
auto it = vec.begin();
for (int i=0; i<n; ++i, ++it){
Pt tmp = vec[i].v;
int res = lower_bound(vec.begin(), vec.end(), Node{vec[i].qd+2, Pt{-tmp.x, -tmp.y}})-it;
ans = min({ans, res, n-res});
}
cout << ans << '\n';
}
return 0;
}
D. Shortest Path Fast Algorithm
大概知道题解要怎么搞,但具体实现还是不太清楚,先坑了qwq
E. Pass the Ball!
很套路的一个题,但可能是因为我FFT板子的问题,导致精度一直有问题卡不过去,后面徐神提出用NTT来修正答案才卡过
首先不难发现每个置换环的贡献是独立的,对于长度为\(m\)的置换环,它对答案的贡献在\(m\)次操作后就会成重复,因此只要考虑\([0,m-1]\)的贡献即可
把式子写出来玩一下发现是个循环卷积,这东西的处理我只隐约地记得一点,后面问了徐神告诉我做法
其实就是把原置换环\(\{a_{0\sim m-1}\}\)翻转后再倍长得到\(\{b_{0\sim 2m-1}\}\),然后求出两个序列的卷积\(\{c_{0\sim 3m-1}\}\)后,其中的\(c_{m-1}\sim c_{2m-2}\)就是\(0\sim m-1\)对应的贡献了
这部分的复杂度是\(O(n\log n)\)的,然后我们发现对于长度相同的置换环可以把贡献合并
再利用经典结论不同的环长度是\(O(\sqrt n)\)的,因此每个询问暴力枚举即可,这部分复杂度\(O(q\sqrt n)\)
但由于这里卷积后的结果大概是\(10^{15}\)级别的因此用开long double
的FFT精度还是不够,但后面祁神搞了组极限数据后发现其实只有很小的误差(大概就是\(\pm1\)左右)
因此我们可以再跑一遍NTT,求出卷积对\(998244353\)取模的结果然后根据这个来微调即可,具体实现可以看代码
我只能说这个思路真是惊为天人,虽然扩大了两倍常数但感觉可以把FFT的使用范围狠狠地扩大,可能说不定什么时候就能用上了
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<vector>
#include<map>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
typedef long double LDB;
const int N=100005,mod=998244353;
int n,q,p[N],nxt[N],vis[N],k,idx[N],cnt; vector <int> coef[N];
namespace Poly
{
const LDB PI=acosl(-1.0L);
struct Complex
{
LDB x,y;
inline Complex (const LDB& X=0,const LDB& Y=0)
{
x=X; y=Y;
}
inline Complex conj(void)
{
return Complex(x,-y);
}
friend inline Complex operator + (const Complex& A,const Complex& B)
{
return Complex(A.x+B.x,A.y+B.y);
}
friend inline Complex operator - (const Complex& A,const Complex& B)
{
return Complex(A.x-B.x,A.y-B.y);
}
friend inline Complex operator * (const Complex& A,const Complex& B)
{
return Complex(A.x*B.x-A.y*B.y,A.x*B.y+A.y*B.x);
}
}A[N<<4],B[N<<4]; int C[N<<4],D[N<<4],lim,p,rev[N<<4];
inline void init(CI n)
{
for (lim=1,p=0;lim<=n;lim<<=1,++p);
for (RI i=0;i<lim;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<p-1);
}
inline void FFT(Complex *f,CI opt)
{
RI i,j,k; for (i=0;i<lim;++i) if (i<rev[i]) swap(f[i],f[rev[i]]);
for (i=1;i<lim;i<<=1)
{
Complex D(cos(PI/i),opt*sin(PI/i));
for (j=0;j<lim;j+=(i<<1))
{
Complex W(1,0); for (k=0;k<i;++k,W=W*D)
{
Complex x=f[j+k],y=W*f[i+j+k];
f[j+k]=x+y; f[i+j+k]=x-y;
}
}
}
if (!~opt)
{
for (i=0;i<lim;++i) f[i].x/=lim,f[i].y/=lim;
}
}
inline int sum(CI x,CI y)
{
return x+y>=mod?x+y-mod:x+y;
}
inline int sub(CI x,CI y)
{
return x-y<0?x-y+mod:x-y;
}
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 void NTT(int *f,CI opt)
{
RI i,j,k; for (i=0;i<lim;++i) if (i<rev[i]) swap(f[i],f[rev[i]]);
for (i=1;i<lim;i<<=1)
{
int m=i<<1,D=quick_pow(3,opt==1?(mod-1)/m:mod-1-(mod-1)/m);
for (j=0;j<lim;j+=m)
{
int W=1; for (RI k=0;k<i;++k,W=1LL*W*D%mod)
{
int x=f[j+k],y=1LL*f[i+j+k]*W%mod;
f[j+k]=sum(x,y); f[i+j+k]=sub(x,y);
}
}
}
if (!~opt)
{
int Inv=quick_pow(lim); for (i=0;i<lim;++i) f[i]=1LL*f[i]*Inv%mod;
}
}
inline void Mul(vector <int>& a)
{
RI i; int n=a.size(); init(3*n);
for (i=0;i<lim;++i) A[i]=B[i]=Complex(),C[i]=D[i]=0;
for (i=0;i<n;++i) A[i].x=B[n-i-1].x=B[n-i-1+n].x=C[i]=D[n-i-1]=D[n-i-1+n]=a[i];
for (FFT(A,1),FFT(B,1),NTT(C,1),NTT(D,1),i=0;i<lim;++i) A[i]=A[i]*B[i],C[i]=1LL*C[i]*D[i]%mod;
for (FFT(A,-1),NTT(C,-1),i=0;i<n;++i)
{
int x=(int)(A[n-1+i].x+0.5),y=C[n-1+i];
while (x%mod<y) ++x; while (x%mod>y) --x;
a[i]=x;
}
}
}
signed main()
{
//freopen("E.in","r",stdin); freopen("E.out","w",stdout);
RI i,j; for (scanf("%lld%lld",&n,&q),i=1;i<=n;++i)
scanf("%lld",&p[i]),nxt[p[i]]=i;
for (i=1;i<=n;++i) if (!vis[i])
{
vector <int> tmp; int x=i;
do
{
tmp.push_back(x); vis[x]=1; x=nxt[x];
} while (x!=i);
Poly::Mul(tmp); int sz=tmp.size();
if (coef[sz].empty()) idx[++cnt]=sz,coef[sz]=tmp; else
for (j=0;j<sz;++j) coef[sz][j]+=tmp[j];
}
for (i=1;i<=q;++i)
{
scanf("%lld",&k); int ret=0;
for (j=1;j<=cnt;++j) ret+=coef[idx[j]][k%idx[j]];
printf("%lld\n",ret);
}
return 0;
}
F. Sandpile on Clique
乐比赛的时候这题徐神写了个卡时交上去一直WA,后面我一看代码发现是没关流同步然后用cin
导致读入花费太多时间,后面卡时用的时间不足,加上流同步就过了233
这题其实开始时我们队没太多想法,就是感觉可以每次找出序列中最大的数然后把它分解掉,如果这个过程重复多次后还没有停止就输出无限循环,感性理解一下挺对的
后面看了题解发现其实就是如果有\(n-1\)次这样的操作还没有结束的话就一定可以无限进行下去,因为考虑\(n\)个人中至少会有\(1\)个人没有被分解过,那么这个人一定会收到至少\(n-1\)个球,那么它下次就可以进行分解
依此类推我们对于所有相邻的\(n-1\)个数,都可以推出至少还有一个数可以出现,因此这个过程时无限的
这里就直接放比赛时写的卡时代码了
#include<bits/stdc++.h>
#define int int64_t
int n, a[1000001], gd = 0;
struct cmp{
inline bool operator ()(const int x, const int y) {
return a[x] < a[y];
}
};
std::priority_queue<int, std::vector<int>, cmp> pq;
int32_t main() {
std::ios::sync_with_stdio(false);
// freopen("1.in", "r", stdin);
double start = clock();
std::cin >> n;
for(int i = 1; i <= n; ++i) std::cin >> a[i], pq.push(i);
while(double(clock() - start) / CLOCKS_PER_SEC <= 0.92l) {
int maxi = pq.top(); pq.pop();
int maxa = a[maxi] + gd;
// std::cout << maxi << std::endl;
if(maxa < n - 1) {
for(int i = 1; i <= n; ++i) std::cout << a[i] + gd << char(i == n ? 10 : 32);
return 0;
}
int d = maxa / (n - 1);
gd += d;
maxa = maxa % (n - 1);
a[maxi] = maxa - gd;
pq.push(maxi);
// for(int i = 1; i <= n; ++i) std::cout << a[i] + gd << char(i == n ? 10 : 32);
}
std::cout << "Recurrent\n";
return 0;
}
G. Cyclic Buffer
徐神看完题秒出了正解的DP状态后我就负责把剩下的转移细节和优化这部分做做就好了
感觉我的DP水平就是不会设状态,如果把正确的状态给我我基本上都能搞出来
因此一般组队比赛的时候有队友想方程我来优化,导致我们队DP题出的都挺多,但一旦自己打(点名CF)就开始挂机坐牢了
扯远了回到这题,我们可以设\(f_{i,0/1}\)表示处理了前\(i\)个数,第\(i\)个数在第\(1\)个位置/第\(k\)个位置的最小移动次数
然后我们还要预处理出\(g_{x,0/1}\)表示当把\(x\)这个数放在第\(1\)个位置/第\(k\)个位置时,可以连带处理掉后面的若干的数,直到的第一个没有处理的数
那么转移的话就是\(f_{i,0}\to f_{g_{i,0},0/1},f_{i,1}\to f_{g_{i,1},0/1}\),而中间移动的代价就是两个方向取小即可
现在的难点就在于如何求出\(g_{x,0/1}\),以求\(g_{x,0}\)为例,我们可以从后往前地维护一个长度为\(k\)的滑动窗口,每次移动窗口就把里面的数都扔进权值线段树中
现在的问题就变成求某个数后面的第一个没出现的数是什么,可以很容易地用线段树上二分得到
然后这题就做完了,总复杂度\(O(n\log n)\),比赛时没管常数写了个1996ms卡过
#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,INF=1e18;
int t,n,k,p[N<<1],pos[N],g[N][2],f[N][2],ans;
class Segment_Tree
{
private:
int mi[N<<2];
inline void pushup(CI now)
{
mi[now]=min(mi[now<<1],mi[now<<1|1]);
}
public:
#define TN CI now=1,CI l=1,CI r=n
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void build(TN)
{
mi[now]=0; if (l==r) return; int mid=l+r>>1; build(LS); build(RS);
}
inline void updata(CI pos,CI mv,TN)
{
if (l==r) return (void)(mi[now]=mv); int mid=l+r>>1;
if (pos<=mid) updata(pos,mv,LS); else updata(pos,mv,RS); pushup(now);
}
inline int query(CI pos,TN)
{
if (r<pos) return -1; if (mi[now]==1) return -1;
if (l==r) return l; int mid=l+r>>1,res=query(pos,LS);
if (~res) return res; return query(pos,RS);
}
#undef TN
#undef LS
#undef RS
}SEG;
signed main()
{
//freopen("G.in","r",stdin); freopen("G.out","w",stdout);
for (scanf("%lld",&t);t;--t)
{
RI i; for (scanf("%lld%lld",&n,&k),i=1;i<=n;++i)
scanf("%lld",&p[i]),f[i][0]=f[i][1]=INF,pos[p[i]]=i,p[i+n]=p[i];
if (n==k) { puts("0"); continue; } ans=INF;
for (SEG.build(),i=1;i<=k;++i) SEG.updata(p[n+i-1],1); g[p[n]][0]=SEG.query(p[n]);
for (i=n-1;i>=1;--i) SEG.updata(p[i],1),SEG.updata(p[i+k],0),g[p[i]][0]=SEG.query(p[i]);
for (SEG.build(),i=1;i<=k;++i) SEG.updata(p[n+1-i+1],1); g[p[n+1]][1]=SEG.query(p[n+1]);
for (i=n+2;i<=2*n;++i) SEG.updata(p[i],1),SEG.updata(p[i-k],0),g[p[i]][1]=SEG.query(p[i]);
//for (i=1;i<=n;++i) printf("g[%lld][0] = %lld\n",i,g[i][0]);
//for (i=1;i<=n;++i) printf("g[%lld][1] = %lld\n",i,g[i][1]);
int fir=-1; for (i=1;i<=n;++i) if (pos[i]>k) { fir=i; break; }
auto move=[&](CI x,CI y)
{
if (x>=y) return min(x-y,n-x+y); return min(y-x,n-y+x);
};
for (f[fir][0]=move(pos[fir],1),f[fir][1]=move(pos[fir],k),i=fir;i<=n;++i)
{
if (!~g[i][0]) ans=min(ans,f[i][0]); else
{
int tmp=pos[g[i][0]]+1-pos[i];
while (tmp<1) tmp+=n; while (tmp>n) tmp-=n;
f[g[i][0]][0]=min(f[g[i][0]][0],f[i][0]+move(tmp,1));
f[g[i][0]][1]=min(f[g[i][0]][1],f[i][0]+move(tmp,k));
}
if (!~g[i][1]) ans=min(ans,f[i][1]); else
{
int tmp=pos[g[i][1]]+k-pos[i];
while (tmp<1) tmp+=n; while (tmp>n) tmp-=n;
f[g[i][1]][0]=min(f[g[i][1]][0],f[i][1]+move(tmp,1));
f[g[i][1]][1]=min(f[g[i][1]][1],f[i][1]+move(tmp,k));
}
}
printf("%lld\n",ans);
}
return 0;
}
H. Permutation on Tree
噫,题目都没看,直接弃疗
I. LCS Spanning Tree
这题纯徐神个人能力,我又是题目都没看
广义SAM的实际应用我只能说不好意思一点不会,但只要是字符串题我们队就无条件相信徐神
#include <bits/stdc++.h>
constexpr int $n = 4000000 + 5;
int go[$n][26], fa[$n], len[$n], las = 1, O = 1;
int insert(char a) {
int c = a - 'a', p = las;
if(go[p][c]) {
int q = go[p][c];
if(len[q] == len[p] + 1) return las = q;
int nq = ++O; len[nq] = len[p] + 1;
for(int i = 0; i < 26; ++i) go[nq][i] = go[q][i];
for(; p && go[p][c] == q; p = fa[p]) go[p][c] = nq;
fa[nq] = fa[q]; fa[q] = nq;
return las = nq;
}
int np = las = ++O;
len[np] = len[p] + 1;
for(int i = 0; i < 26; ++i) go[np][i] = 0;
for(; p && !go[p][c]; p = fa[p]) go[p][c] = np;
if(!p) return fa[np] = 1, las;
int q = go[p][c];
if(len[q] == len[p] + 1) return fa[np] = q, las;
int nq = ++O; len[nq] = len[p] + 1;
for(int i = 0; i < 26; ++i) go[nq][i] = go[q][i];
fa[nq] = fa[q]; fa[np] = fa[q] = nq;
for(; p && go[p][c] == q; p = fa[p]) go[p][c] = nq;
return las;
}
int n, Fa[$n];
std::string s[$n];
std::vector<std::pair<int, int> > host[$n];
std::priority_queue<std::pair<int, int> > pq;
inline int father(int a) {
if(Fa[a] == a) return Fa[a];
return Fa[a] = father(Fa[a]);
}
int main() {
// freopen("1.in", "r", stdin);
std::ios::sync_with_stdio(false);
std::cin >> n;
for(int i = 1; i <= n; ++i) Fa[i] = i;
for(int i = 1; i <= n; ++i) {
las = 1;
std::cin >> s[i];
for(char c: s[i]) insert(c);
}
for(int i = 1; i <= n; ++i) {
int now = 1;
for(size_t j = 0; j < s[i].size(); ++j)
host[now = go[now][s[i][j] - 'a']].push_back({j + 1, i});
}
// return 0;
for(int i = 1; i <= O; ++i) if(host[i].size()) {
std::sort(host[i].begin(), host[i].end());
for(int j = 0; j < host[i].size() - 1; ++j)
pq.push({host[i][j].first, i});
pq.push({host[i][0].first, i});
}
long long signed int ans = 0;
while(pq.size()) {
auto [_, i] = pq.top(); pq.pop();
if(!host[i].size()) continue;
if(host[i].size() == 1) {
// std::cerr << "debug " << i << ' ' << host[i].front().first << ' ' << _ << std::endl;
if(fa[i]) {
if(host[fa[i]].size()) pq.push({host[fa[i]].back().first, fa[i]});
else pq.push({len[fa[i]], fa[i]});
host[fa[i]].push_back( std::pair<int, int>(len[fa[i]], host[i][0].second) );
}
host[i].pop_back();
continue;
}
// std::cerr << "debug " << i << ' ' << host[i].end()[-2].first << ' ' << _ << std::endl;
auto [la, a] = host[i].back(); host[i].pop_back();
auto [lb, b] = host[i].back();
a = father(a), b = father(b);
if(a != b) {
Fa[a] = b;
// std::cout << "merge " << a << ' ' << b << ' ' << lb << std::endl;
ans += lb;
}
}
std::cout << ans << std::endl;
return 0;
}
J. Colorful Tree
看这个过题数就是全场最难的题了,直接溜溜
K. Link-Cut Tree
签到题,注意到其实就是用并查集维护什么时候成环就直接退出输出方案即可
刚开始想着怎么用并查集维护路径和方便,后来一想妈的直接暴力把森林建出来在上面爆搜就行了
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=100005;
int t,n,m,x,y,fa[N],pre[N],lst[N]; vector <pi> v[N]; vector <int> ans;
inline int getfa(CI x)
{
return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
inline void DFS(CI now,CI st,CI tar)
{
if (now==tar)
{
for (int x=tar;x!=st;x=pre[x]) ans.push_back(lst[x]); return;
}
for (auto [to,id]:v[now]) if (!~pre[to]) pre[to]=now,lst[to]=id,DFS(to,st,tar);
}
int main()
{
//freopen("K.in","r",stdin); freopen("K.out","w",stdout);
for (scanf("%d",&t);t;--t)
{
RI i; for (scanf("%d%d",&n,&m),i=1;i<=n;++i) pre[i]=-1,fa[i]=i,v[i].clear();
bool flag=0; for (i=1;i<=m;++i)
{
if (scanf("%d%d",&x,&y),flag) continue;
if (getfa(x)!=getfa(y)) fa[getfa(x)]=getfa(y),v[x].push_back(pi(y,i)),v[y].push_back(pi(x,i));
else
{
flag=1; ans.clear(); pre[x]=0; DFS(x,x,y);
ans.push_back(i); sort(ans.begin(),ans.end());
for (RI j=0;j<ans.size();++j) printf("%d%c",ans[j]," \n"[j==ans.size()-1]);
}
}
if (!flag) puts("-1");
}
}
Postscript
队友都太强辣,狠狠地带飞我
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步