Preface
唐完了的一集,被前中期题花式腐乳以至于中期一度被同校队伍打出 \(n+3\)
最后 4h 时堪堪写完 8 个题,最后 1h 冲刺众数那个题然后写一半方法假了直接下班
当然还有个难绷的是传送那个题和我今年给新生拉的数据结构专题的一个题几乎一样,把代码拉来改下输入输入就过了,12min 抢到一血可海星
循环位移
徐神刚开始写了个 SAM 然后发现被卡空间了,然后后面一顿修改 WA 了两发后红温了,就喊我上去写了个 Hash
Hash 的做法就非常无脑,把 \(A\) 的所有循环位移串的 Hash 值扔进 set
里,然后枚举 \(B\) 的所有长为 \(|A|\) 的子串检验即可
#include<cstdio>
#include<iostream>
#include<set>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
typedef long long LL;
const int N=2100000,mod1=998244353,mod2=1e9+7;
struct Hasher
{
int x,y;
inline Hasher(CI X=0,CI Y=0)
{
x=X; y=Y;
};
inline LL val(void)
{
return ((1LL*x)<<31LL)|(1LL*y);
}
friend inline Hasher operator + (const Hasher& A,const Hasher& B)
{
return Hasher((A.x+B.x)%mod1,(A.y+B.y)%mod2);
}
friend inline Hasher operator - (const Hasher& A,const Hasher& B)
{
return Hasher((A.x-B.x+mod1)%mod1,(A.y-B.y+mod2)%mod2);
}
friend inline Hasher operator * (const Hasher& A,const Hasher& B)
{
return Hasher(1LL*A.x*B.x%mod1,1LL*A.y*B.y%mod2);
}
}h[N],pw[N]; int t; char a[N],b[N];
const Hasher seed=(31,131);
inline Hasher get_hash(CI l,CI r)
{
return h[r]-h[l-1]*pw[r-l+1];
}
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%s%s",a+1,b+1); int n=strlen(a+1),m=strlen(b+1);
RI i; for (i=1;i<=n;++i) a[n+i]=a[i];
for (pw[0]=Hasher(1,1),i=1;i<=2*n;++i) pw[i]=pw[i-1]*seed;
for (i=1;i<=2*n;++i) h[i]=h[i-1]*seed+Hasher(a[i]-'A',a[i]-'A');
set <LL> ext; for (i=1;i<=n;++i) ext.insert(get_hash(i,i+n-1).val());
for (i=1;i<=m;++i) h[i]=h[i-1]*seed+Hasher(b[i]-'A',b[i]-'A');
int ans=0; for (i=1;i+n-1<=m;++i) ans+=ext.count(get_hash(i,i+n-1).val());
printf("%d\n",ans);
}
return 0;
}
星星
暴力背包 DP 即可,注意枚举的界写的紧一点不然可能会 TLE
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int INF = (int)1e18+5;
const int N = 4005;
int t, n, k;
int dp[N];
int wt[5];
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
cin >> t;
while (t--){
cin >> n >> k;
for (int i=0; i<=k; ++i) dp[i]=INF;
dp[0] = 0;
for (int i=1; i<=n; ++i){
cin >> wt[1] >> wt[2] >> wt[3] >> wt[4];
for (int w=4*i; w>=0; --w){
for (int j=4; j>0; --j){
if (w-j<0) continue;
dp[w] = min(dp[w-j]+wt[j], dp[w]);
}
}
}
cout << dp[k] << '\n';
}
return 0;
}
树
很套路的题,考虑已经维护出了一个集合的答案,往其中加入一个数 \(x\) 会发生什么
通过分类讨论来去除 \(\max\) 的影响,讨论集合中一个数 \(y\) 的贡献:
- \(y\le x\) 时,贡献为 \(x\times (x-y)\),需要求出集合中 \(\le x\) 的数的数量以及 \(\le x\) 的数的和;
- \(y>x\) 时,贡献为 \(y\times (y-x)\),需要求出集合中 \(> x\) 的数的和以及 \(> x\) 的数的平方和;
通过 DSU on Tree
以及树状数组,可以做到小常数的 \(O(n\log^2 n)\)
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
typedef unsigned long long u64;
const int N=1e6+5;
int n,m,x,y,a[N],sz[N],son[N]; vector <int> v[N]; u64 ans,ret;
class Tree_Array
{
private:
u64 bit[N];
public:
#define lowbit(x) (x&-x)
inline void upt(RI x,const u64& y)
{
for (;x<=m;x+=lowbit(x)) bit[x]+=y;
}
inline u64 get(RI x,u64 ret=0)
{
for (;x;x-=lowbit(x)) ret+=bit[x]; return ret;
}
inline u64 query(CI l,CI r)
{
if (l>r) return 0ull; return get(r)-get(l-1);
}
#undef lowbit
}CNT,SUM,SQR;
inline void DFS(CI now=1,CI fa=0)
{
sz[now]=1; for (auto to:v[now]) if (to!=fa)
if (DFS(to,now),sz[now]+=sz[to],sz[to]>sz[son[now]]) son[now]=to;
}
inline void work(CI x,CI sgn)
{
u64 dlt=(CNT.query(1,a[x])*a[x]-SUM.query(1,a[x]))*a[x]+SQR.query(a[x]+1,m)-SUM.query(a[x]+1,m)*a[x];
if (sgn>0) ret+=dlt; else ret-=dlt;
CNT.upt(a[x],u64(sgn)); SUM.upt(a[x],u64(sgn)*a[x]); SQR.upt(a[x],u64(sgn)*a[x]*a[x]);
}
inline void travel(CI now,CI fa,CI mv)
{
work(now,mv); for (auto to:v[now]) if (to!=fa) travel(to,now,mv);
}
inline void DSU(CI now=1,CI fa=0,CI flag=1)
{
for (auto to:v[now]) if (to!=fa&&to!=son[now]) DSU(to,now,0);
if (son[now]) DSU(son[now],now,1);
for (auto to:v[now]) if (to!=fa&&to!=son[now]) travel(to,now,1);
work(now,1); ans^=2ull*ret; //printf("%d = %llu\n",now,2ull*ret);
if (!flag) travel(now,fa,-1);
}
int main()
{
RI i; for (scanf("%d",&n),i=1;i<n;++i)
scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for (i=1;i<=n;++i) scanf("%d",&a[i]),m=max(m,a[i]);
return DFS(),DSU(),printf("%llu",ans),0;
}
传送
很经典的线段树分治+可撤销并查集+懒标记维护
对时间分治后考虑在线段树的叶子节点将 \(1\) 所在的联通块的根节点打上对应时间的标记,在并查集撤销时将标记下传即可
#include<cstdio>
#include<iostream>
#include<vector>
#include<utility>
#define RI register int
#define CI const int&
using namespace std;
typedef long long LL;
typedef pair <int,int> pi;
const int N=600005;
struct Data
{
int x,y,szy;
inline Data(CI X=0,CI Y=0,CI SZY=0)
{
x=X; y=Y; szy=SZY;
}
}stk[N*20]; int n,m,l[N],r[N],x,y,a,b,top,fa[N],sz[N]; LL tag[N];
inline int getfa(CI x)
{
return fa[x]!=x?getfa(fa[x]):x;
}
inline void merge(int x,int y)
{
x=getfa(x); y=getfa(y); if (x==y) return;
if (sz[x]>sz[y]) swap(x,y); stk[++top]=Data(x,y,sz[y]);
fa[x]=y; sz[y]+=sz[x]==sz[y]; tag[x]-=tag[y];
}
inline void revoke(CI tmp)
{
while (top>tmp)
{
int x=stk[top].x,y=stk[top].y,szy=stk[top].szy;
--top; fa[x]=x; sz[y]=szy; tag[x]+=tag[y];
}
}
class Segment_Tree
{
private:
vector <pi> v[N<<2];
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 insert(CI beg,CI end,const pi& it,TN)
{
if (beg<=l&&r<=end) return v[now].push_back(it); int mid=l+r>>1;
if (beg<=mid) insert(beg,end,it,LS); if (end>mid) insert(beg,end,it,RS);
}
inline void solve(TN)
{
int tmp=top; for (auto [x,y]:v[now]) merge(x,y);
if (l==r) return tag[getfa(1)]+=l,revoke(tmp);
int mid=l+r>>1; solve(LS); solve(RS); revoke(tmp);
}
#undef TN
#undef LS
#undef RS
}SEG;
int main()
{
//freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
RI i; for (scanf("%d%d",&n,&m),i=1;i<=n;++i) fa[i]=i,sz[i]=1;
for (i=1;i<=m;++i)
{
scanf("%d%d%d%d",&x,&y,&a,&b);
SEG.insert(a,b,make_pair(x,y));
}
LL ans=0; for (SEG.solve(),i=1;i<=n;++i) ans^=tag[i];
return printf("%lld",ans),0;
}
博弈
徐神敏锐地观察发现当且仅当出现次数为奇数的字符有 \(\le 1\) 个时可能会打到最后一轮,否则游戏总会提前结束并且由于对称性,两人的胜率都是 \(\frac{1}{2}\)
否则分情况讨论,当所有字符出现次数均为偶数时,只需算出平局的概率 \(p_1\),则获胜的概率为 \(\frac{1-p_1}{2}\)
当存在恰好一种字符出现次数为奇数时,算出除去一个该字符后平局的概率 \(p_2\),则获胜的概率为 \(\frac{1+p_2}{2}\)
\(p_1,p_2\) 都容易用组合意义推出表达式,这里不再赘述
#include <bits/stdc++.h>
using llsi = long long signed int;
constexpr int $n = 10000007;
constexpr llsi mod = 998244353;
constexpr llsi ksm(llsi a, llsi b) {
llsi res = 1;
while(b) {
if(b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
int fac[$n] = {1}, facinv[$n];
llsi C(const std::vector<int> S, bool inv = false) {
llsi tot = 0, res = 1;
if(inv) for(auto c: S) {
res = res * fac[c] % mod;
tot += c;
} else for(auto c: S) {
res = res * facinv[c] % mod;
tot += c;
}
if(inv) return facinv[tot] * res % mod;
else return fac[tot] * res % mod;
}
int main() {
std::ios::sync_with_stdio(false);
for(int i = 1; i <= 10000000; ++i) fac[i] = llsi(fac[i - 1]) * i % mod;
facinv[10000000] = ksm(fac[10000000], mod - 2);
for(int i = 10000000; i >= 1; --i) facinv[i - 1] = llsi(facinv[i]) * i % mod;
// std::cout << 1ll * fac[10] * facinv[8] % mod << char(10);
constexpr llsi inv2 = ksm(2, mod - 2);
int T; std::cin >> T; while(T--) {
int n; std::cin >> n;
int odd_count = 0;
std::vector<int> a(n);
std::string _;
for(auto &a: a) std::cin >> _ >> a, odd_count += (a & 1);
if(odd_count >= 2) {
std::cout << inv2 << char(10);
continue;
}
llsi p = C(a, true);
for(auto &a: a) a /= 2;
p = p * C(a, false) % mod;
if(odd_count) {
std::cout << ((mod + 1 - p) * inv2 % mod + p) % mod << char(10);
} else {
std::cout << (mod + (mod + 1 - p) * inv2 % mod) % mod << char(10);
}
}
return 0;
}
序列立方
很有思维含量的一个题,总感觉我做过平方版本的题目来着,这题就是把平方的做法搬过来
考虑将原问题抽象为以下问题:在序列中选出三个相同的子序列,它们相同的方案数
不难发现这个问题可以 DP 求解,令 \(f_{i,j,k}\) 表示当前三个子序列的末尾分别为 \(a_i,a_j,a_k\) 的方案数,暴力转移显然是枚举 \(a_x=a_y=a_z\) 的 \((x,y,z)\) 并转移
但这样是 \(O(n^6)\) 的,后面徐神发现可以用高维前缀和优化该过程,即可将复杂度降为 \(O(n^3)\)
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=255,mod=998244353;
int n,a[N],f[N][N][N];
inline void inc(int& x,CI y)
{
if ((x+=y)>=mod) x-=mod;
}
inline void dec(int& x,CI y)
{
if ((x-=y)<0) x+=mod;
}
int main()
{
RI i,j,k; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]);
f[0][0][0]=1; int ans=0;
for (i=1;i<=n;++i) for (j=1;j<=n;++j) for (k=1;k<=n;++k)
{
inc(f[i][j][k],f[i-1][j][k]);
inc(f[i][j][k],f[i][j-1][k]);
inc(f[i][j][k],f[i][j][k-1]);
dec(f[i][j][k],f[i-1][j-1][k]);
dec(f[i][j][k],f[i-1][j][k-1]);
dec(f[i][j][k],f[i][j-1][k-1]);
inc(f[i][j][k],f[i-1][j-1][k-1]);
if (a[i]==a[j]&&a[i]==a[k])
{
inc(ans,f[i][j][k]);
inc(f[i+1][j+1][k+1],f[i][j][k]);
}
}
return printf("%d",ans),0;
}
三元环
比赛的时候失了智没想到题解中这么显然的容斥做法,鉴定为纯纯的彩笔
由于这个图是个竞赛图,那么任意选出三个点,它们要么构成三元环,要么存在一个点入度为 \(2\) 就构不成三元环
令 \(in_i\) 表示点 \(i\) 的入度,则答案为 \(C_n^3-\sum_{i=1}^n C_{in_i}^2\),计算 \(in_i\) 可以转化为经典的三维偏序求解
#include<cstdio>
#include<iostream>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
struct ifo
{
int f,g,id;
friend inline bool operator < (const ifo& A,const ifo& B)
{
return A.f<B.f;
}
}p[N]; int n,f[N],g[N],in[N];
class Tree_Array
{
private:
int bit[N];
public:
#define lowbit(x) (x&-x)
inline int get(RI x,int ret=0)
{
for (;x;x-=lowbit(x)) ret+=bit[x]; return ret;
}
inline void add(RI x,CI y)
{
for (;x<=n;x+=lowbit(x)) bit[x]+=y;
}
inline void output(void)
{
for (RI i=1;i<=n;++i) printf("%lld%c",bit[i]," \n"[i==n]);
}
#undef lowbit
}BIT;
inline void solve0(void)
{
RI i; for (i=n;i>=1;--i) in[i]+=BIT.get(f[i]),BIT.add(f[i],1);
for (i=1;i<=n;++i) BIT.add(f[i],-1);
for (i=n;i>=1;--i) in[i]+=BIT.get(g[i]),BIT.add(g[i],1);
for (i=1;i<=n;++i) BIT.add(g[i],-1);
}
inline void solve1(CI l=1,CI r=n)
{
if (l>=r) return; int mid=l+r>>1;
solve1(l,mid); solve1(mid+1,r);
sort(p+l,p+mid+1); sort(p+mid+1,p+r+1);
RI i,j; for (i=l,j=mid+1;j<=r;++j)
{
while (i<=mid&&p[i].f<p[j].f) BIT.add(p[i++].g,1);
in[p[j].id]+=BIT.get(p[j].g-1);
}
for (j=l;j<i;++j) BIT.add(p[j].g,-1);
}
inline void solve2(CI l=1,CI r=n)
{
if (l>=r) return; int mid=l+r>>1;
solve2(l,mid); solve2(mid+1,r);
sort(p+l,p+mid+1); sort(p+mid+1,p+r+1);
RI i,j; for (i=l,j=mid+1;i<=mid;++i)
{
while (j<=r&&p[i].f>=p[j].f) BIT.add(p[j++].g,1);
in[p[i].id]-=BIT.get(p[i].g);
}
for (i=mid+1;i<j;++i) BIT.add(p[i].g,-1);
}
signed main()
{
RI i; for (scanf("%lld",&n),i=1;i<=n;++i) scanf("%lld",&f[i]);
for (i=1;i<=n;++i) scanf("%lld",&g[i]); solve0();
for (i=1;i<=n;++i) p[i].f=f[i],p[i].g=g[i],p[i].id=i; solve1();
for (i=1;i<=n;++i) p[i].f=f[i],p[i].g=g[i],p[i].id=i; solve2();
long long ans=1LL*n*(n-1)*(n-2)/6LL;
for (i=1;i<=n;++i) ans-=1LL*in[i]*(in[i]-1)/2LL;
//for (i=1;i<=n;++i) printf("%lld%c",in[i]," \n"[i==n]);
return printf("%lld",ans),0;
}
位运算
签到,不难发现每一位独立,预处理下这一位为 \(0/1\) 的方案数即可
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t,n,k,w[2];
int main()
{
RI i; for (RI i=0;i<2;++i)
for (int a=0;a<2;++a)
for (int b=0;b<2;++b)
for (int c=0;c<2;++c)
for (int d=0;d<2;++d)
if (((((a&b)^c))|d)==i) ++w[i];
for (scanf("%d",&t);t;--t)
{
scanf("%d%d",&n,&k); long long ans=1;
for (i=0;i<k;++i) ans*=w[(n>>i)&1];
printf("%lld\n",ans);
}
return 0;
}
数位的关系
最后时间不够了我到比赛结束才看到这个题,感觉就是个比较显然的数位 DP
但这玩意之前都是交给徐神写的,而且感觉细节有点多不想补了,先坑着再说
众数
最神秘的一个题,赛时只想到了利用随机生成的性质得到的笛卡尔树是趋于平衡的,没想到可以直接利用这个性质求答案
注意到随机情况下答案极大概率是大数,因此直接把前 \(K=25\) 大的数拉出来算一下出现次数大概率就能得到答案
具体实现时可以用堆维护区间,每次取出最大值最大的区间 \([l,r]\) 后根据最大值所在的位置 \(p\) 分裂区间即可,最大值 \(a_p\) 的贡献即为 \((p-l+1)\times (r-p+1)\)
复杂度 \(O(nK\log n)\),代码十分好写
#include<cstdio>
#include<iostream>
#include<queue>
#include<utility>
#define int long long
#define RI register int
#define CI const int&
#define mp make_pair
#define fi first
#define se second
using namespace std;
typedef pair <int,int> pi;
const int N=200005,K=25;
int t,n,q,a[N],l,r;
namespace RMQ
{
pi mx[N][20]; int _log[N];
inline void init(CI n)
{
RI i,j; for (_log[0]=-1,i=1;i<=n;++i)
_log[i]=_log[i>>1]+1,mx[i][0]=pi(a[i],i);
for (j=1;(1<<j)<=n;++j)
for (i=1;i+(1<<j)-1<=n;++i)
mx[i][j]=max(mx[i][j-1],mx[i+(1<<j-1)][j-1]);
}
inline pi query(CI l,CI r)
{
int k=_log[r-l+1]; return max(mx[l][k],mx[r-(1<<k)+1][k]);
}
}
using namespace RMQ;
signed main()
{
for (scanf("%lld",&t);t;--t)
{
RI i,j; for (scanf("%lld%lld",&n,&q),i=1;i<=n;++i) scanf("%lld",&a[i]);
int ans=0; for (init(n),i=1;i<=q;++i)
{
scanf("%lld%lld",&l,&r); pi ret=pi(0,0);
priority_queue <pair <pi,pi>> hp;
hp.push(mp(query(l,r),pi(l,r)));
for (j=1;j<=K&&!hp.empty();++j)
{
int val=hp.top().fi.fi,cnt=0;
while (!hp.empty()&&hp.top().fi.fi==val)
{
auto [l,r]=hp.top().se; int p=hp.top().fi.se;
cnt+=(p-l+1)*(r-p+1); hp.pop();
if (l<=p-1) hp.push(mp(query(l,p-1),pi(l,p-1)));
if (p+1<=r) hp.push(mp(query(p+1,r),pi(p+1,r)));
}
ret=max(ret,pi(cnt,val));
}
//printf("%lld = (%lld,%lld)\n",i,ret.fi,ret.se);
ans^=i*ret.se;
}
printf("%lld\n",ans);
}
return 0;
}
树上的 mex
防 AK 题,鉴定为弃疗
并
前面祁神去写了线段树扫描线维护矩形面积并的做法,但这样难以维护贡献而且时间复杂度也多个 \(\log\),后面我看了眼后发现可以直接离线用二维前缀和做
考虑将横纵坐标离散化,利用二位前缀和可以快速求出每个区域被多少个矩形覆盖了,并令 \(g_t\) 表示被 \(t\) 个矩形覆盖的面积
最后枚举选出的矩形数量 \(k\),则被覆盖的面积并(即至少覆盖一次的面积)的期望为:
\[\sum_{t=1}^n (1-\frac{C_{n-t}^k}{C_{n}^k})\times g_t \]复杂度 \(O(n^2)\)
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int MOD = 998244353, inv2 = 499122177;
const int mod = MOD;
const int INF = (int)1e18+5;
void add(int &x, int a){if ((x+=a)>=MOD) x-=MOD;}
int powe(int x, int p){
int res=1;
while (p>0){if (p&1) res=res*x%MOD; x=x*x%MOD; p>>=1;}
return res;
}
const int N = 4005;
int fac[N], invf[N];
int n, mp[N][N];
int area[N];
vector<int> hshx, hshy;
struct Node{
int x1, y1, x2, y2;
}nd[N];
signed main(){
ios::sync_with_stdio(0); cin.tie(0);
fac[0]=fac[1]=invf[0]=invf[1]=1;
for (int i=2; i<N; ++i) fac[i]=fac[i-1]*i%MOD;
invf[N-1] = powe(fac[N-1], MOD-2);
for (int i=N-1; i>2; --i) invf[i-1]=invf[i]*i%MOD;
cin >> n;
hshx.push_back(-1); hshy.push_back(-1);
for (int i=0; i<n; ++i){
int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
nd[i] = Node{x1, y1, x2, y2};
hshx.push_back(x1), hshy.push_back(y1);
hshx.push_back(x2), hshy.push_back(y2);
}
sort(hshx.begin(), hshx.end()); hshx.erase(unique(hshx.begin(), hshx.end()), hshx.end());
sort(hshy.begin(), hshy.end()); hshy.erase(unique(hshy.begin(), hshy.end()), hshy.end());
for (int i=0; i<n; ++i){
int xx1 = lower_bound(hshx.begin(), hshx.end(), nd[i].x1) - hshx.begin();
int xx2 = lower_bound(hshx.begin(), hshx.end(), nd[i].x2) - hshx.begin();
int yy1 = lower_bound(hshy.begin(), hshy.end(), nd[i].y1) - hshy.begin();
int yy2 = lower_bound(hshy.begin(), hshy.end(), nd[i].y2) - hshy.begin();
mp[xx1][yy1]++; mp[xx1][yy2]--;
mp[xx2][yy1]--; mp[xx2][yy2]++;
}
int szx=hshx.size(), szy=hshy.size();
for (int i=1; i<szx; ++i){
for (int j=1; j<szy; ++j){
mp[i][j] = mp[i][j] + mp[i-1][j] + mp[i][j-1] - mp[i-1][j-1];
add(area[mp[i][j]], (hshx[i+1]-hshx[i])*(hshy[j+1]-hshy[j])%MOD);
}
}
for (int k=1; k<=n; ++k){
int ans=0;
for (int t=1; t<=n; ++t){
if (n-t-k>=0) add(ans, area[t]*(MOD+1 - fac[n-t]*invf[n-t-k]%MOD*fac[n-k]%MOD*invf[n]%MOD)%MOD);
else add(ans, area[t]);
}
cout << ans << '\n';
}
return 0;
}
Postscript
感觉这场好多数据结构啊,但还是打的一坨,下次遇到一堆计数的不是更加要被打出屎来
标签:钉耙,CI,now,const,int,编程,2024,include,define From: https://www.cnblogs.com/cjjsb/p/18312365