The 3rd Universal Cup. Stage 21: Ōokayama

Preface

神秘地抽到了一个毒瘤场,只能说以后真得避雷霓虹的比赛啊,纯纯的牢底坐穿

打完想补题发现没有英文 Tutorial,甚至官方的日语 Tutorial 里还有一些题是没的,索性直接开摆


B. Self Checkout

因为发病了一直再考虑一种不存在的虚空情况,后面写到一半发现原来是不存在的,直接人傻了

首先 \(1\) 只能出现在末尾且只能有一个;考虑剩下部分的序列一定是一个由 \(2,3\) 连续段交错的

对于一段连续的 \(k\)\(3\),不难发现其合法方案数是一个坐标系上走路的经典问题,推一下方案数是 \(C_{2k}^k-C_{2k}^{k-2}\)

对于非末尾的连续的 \(2\),显然方案数唯一(即原本由一段连续的 \(1\) 构成),且简单分析后可以发现这些 \(2\) 将前后连续的 \(3\) 段隔断,使得每部分的方案数独立

而对于末尾的 \(2\),可能存在将其中若干对 \(1\) 改为 \(2\) 的情况,本来以为会导致前面的一段 \(3\) 方案数变化的,但后面仔细思考发现只有全 \(2\) 的一种情况有影响,讨论下贡献即可

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
#define fi first
#define se second
using namespace std;
typedef pair <int,int> pi;
const int N=2e6+5,mod=998244353;
int n,s[N],fact[N],ifac[N];
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 init(CI n)
{
    fact[0]=1; for (RI i=1;i<=n;++i) fact[i]=1LL*fact[i-1]*i%mod;
    ifac[n]=quick_pow(fact[n]); for (RI i=n-1;i>=0;--i) ifac[i]=1LL*ifac[i+1]*(i+1)%mod;
}
inline int C(CI n,CI m)
{
    if (n<0||m<0||n<m) return 0;
    return 1LL*fact[n]*ifac[m]%mod*ifac[n-m]%mod;
}
inline int calc(CI a,CI b)
{
    return (C(a+b,a)-C(a+b,b-2)+mod)%mod;
}
int main()
{
    scanf("%d",&n); init(2*n);
    for (RI i=1;i<=n;++i) scanf("%d",&s[i]);
    if (n==1) return printf("%d",s[1]==1?1:2),0;
    int cnt1=0;
    for (RI i=1;i<=n;++i) cnt1+=(s[i]==1);
    if (cnt1>=2) return puts("0"),0;
    if (cnt1==1&&s[n]!=1) return puts("0"),0;
    vector <pi> vec; vec.push_back(pi(s[1],1));
    for (RI i=2;i<=n;++i)
    {
        if (s[i]==1) continue;
        if (s[i]==vec.back().fi) ++vec.back().se;
        else vec.push_back(pi(s[i],1));
    }
    int ans=1;
    for (auto [num,cnt]:vec)
    {
        if (num==2) continue;
        ans=1LL*ans*calc(cnt,cnt)%mod;
    }
    if (cnt1==1) return printf("%d",ans),0;
    if (vec.back().fi==2)
    {
        int pre3=vec.size()>1?vec[vec.size()-2].se:0;
        int coef=1LL*ans*quick_pow(calc(pre3,pre3))%mod;
        ans=(1LL*coef*calc(pre3+vec.back().se,pre3)%mod+1LL*ans*vec.back().se%mod)%mod;
    }
    return printf("%d",ans),0;
}

C. Segment Tree

考虑先用类似线段树的方式维护出每条长边连接的两点间实际的最短路,这个很容易支持修改

在询问的时候,考虑从底向上枚举经过哪一层走过去的,为了方便实现可以转化为两个点同时走直到碰面

具体地,我们可以维护一个四元组 \((l,r,dl,dr)\) 表示当前点能走到的区间为 \([l,r]\),到两个端点的距离分别为 \(dl,dr\),适时合并贡献即可

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1<<20;
int n,q,M,c[N],mn[N];
int main()
{
    scanf("%d",&n); M=(1<<n+1)-1;
    for (RI i=1;i<=M;++i) scanf("%d",&c[i]),mn[i]=c[i];
    for (RI i=M>>1;i>=1;--i) mn[i]=min(mn[i],mn[i<<1]+mn[i<<1|1]);
    // for (RI i=1;i<=M;++i) printf("%d%c",mn[i]," \n"[i==M]);
    for (scanf("%d",&q);q;--q)
    {
        int opt,x,y; scanf("%d%d%d",&opt,&x,&y);
        if (opt==1)
        {
            c[x]=y; if (x>(M>>1)) mn[x]=c[x],x>>=1;
            for (;x>0;x>>=1) mn[x]=min(c[x],mn[x<<1]+mn[x<<1|1]);
        } else
        {
            x+=(1<<n); y+=(1<<n)-1;
            int ans=x==y?c[x]:1e9;
            static int dlx[20],drx[20],dly[20],dry[20];
            dlx[n]=0; drx[n]=c[x]; dly[n]=c[y]; dry[n]=0;
            for (RI i=n-1;i>=0;--i)
            {
                auto upt=[&](CI p,int* dl,int* dr)
                {
                    if (p&1)
                    {
                        dl[i]=min(dl[i+1]+mn[p^1],dr[i+1]+c[p>>1]);
                        dr[i]=min(dr[i+1],dl[i+1]+mn[p^1]+c[p>>1]);
                    } else
                    {
                        dl[i]=min(dl[i+1],dr[i+1]+mn[p^1]+c[p>>1]);
                        dr[i]=min(dr[i+1]+mn[p^1],dl[i+1]+c[p>>1]);
                    }
                };
                upt(x,dlx,drx); upt(y,dly,dry);
                if (x!=y&&(x>>1)==(y>>1)) ans=min(ans,drx[i+1]+dly[i+1]);
                x>>=1; y>>=1;
                if (x==y) ans=min(ans,min(dlx[i]+dly[i],drx[i]+dry[i]));
            }
            printf("%d\n",ans);
        }
    }
    return 0;
}

E. ReTravel

首先不难观察到最后一定存在一种最优的路径,使得经过的点都在 \(n\) 个点构成的网格图上,即至少存在一个点与之横/纵坐标相同

由数据范围想到区间 DP,令 \(f_{l,r}\) 表示从起点走完区间 \([l,r]\) 中的所有点需要的最小贡献,显然 \(f_{i,i}=x_i+y_i\)

考虑两个点的情况,\(f_{k,k+1}=\max(x_k,x_{k+1})+\max(y_k,y_{k+1})=x_k+y_k+x_{k+1}+y_{k+1}-\min(x_k,x_{k+1})-\min(y_k,y_{k+1})\),即我们的策略是先走到 \((\min(x_k,x_{k+1}),\min(y_k,y_{k+1}))\) 这个点再分别走到两个点

那么画一下多个点的情况其实是等价的,对于一段区间 \([l,r]\) 我们就把它的参照点看作在 \((\min(x_k),\min(y_k)),k\in [l,r]\) 的一个虚拟点即可,这样把区间归纳为点后用经典的区间 DP 找最优分割位置即可

#include<cstdio>
#include<iostream>
#include<cstring>
#define RI register int
#define CI const int&
#define int long long
using namespace std;
const int N=505;
int n,x[N],y[N],f[N][N];
inline int DP(CI l,CI r)
{
    if (l==r) return x[l]+y[l];
    if (~f[l][r]) return f[l][r];
    int ret=1e18; for (RI k=l;k<r;++k)
    ret=min(ret,DP(l,k)+DP(k+1,r));
    int mnx=x[r],mny=y[r];
    for (RI i=l;i<r;++i) mnx=min(mnx,x[i]),mny=min(mny,y[i]);
    return f[l][r]=ret-(mnx+mny);
}
signed main()
{
    scanf("%lld",&n); memset(f,-1,sizeof(f));
    for (RI i=1;i<=n;++i) scanf("%lld%lld",&x[i],&y[i]);
    return printf("%lld",DP(1,n)),0;
}

L. Long Sequence Inversion 2

最战犯的一集,开局扔了基本已经会了的 B 后跟榜做 L,结果坐牢了半天发现不会,后面扔给徐神就直接秒了,我直接躺好了

#include <bits/stdc++.h>

using llsi = long long signed int;

constexpr llsi mod = 998244353;

struct fwt {
    size_t n;
    std::vector<int> c;

    fwt(size_t n): c(n + 1, 0) { this->n = n; }
    
    void add(int p) {
        p += 1;
        for(int i = p; i <= n; i += i&-i) c[i] += 1;
    }

    llsi sum(int p) {
        p += 1;
        llsi res = 0;
        for(int i = p; i >  0; i -= i&-i) res += c[i];
        return res;
    }
};

llsi inversion_calc(const std::vector<int> &p) {
    fwt c(p.size());
    llsi res = 0;
    for(int i = 0; i < p.size(); ++i) {
        res += i - c.sum(p[i]);
        c.add(p[i]);
    }

    return res % mod;
}

constexpr llsi ksm(llsi a, llsi b) {
    llsi c = 1;
    while(b) {
        if(b & 1) c = c * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return c;
}

constexpr llsi inv2 = ksm(2, mod - 2);

llsi l, b;
std::vector<int> p, ip;
std::vector<std::vector<int>> v;

int main() {
    std::cin >> l >> b;
    p.resize(l); ip.resize(l);
    for(auto &p: p) std::cin >> p;
    for(int i = 0; i < l; ++i) ip[p[i]] = i;
    v.assign(l, std::vector<int>(b));
    for(auto &v: v) for(auto &v: v) std::cin >> v;

    // llsi ans = inversion_calc(v[ip[0]]);
    llsi ans = 0;

    fwt c(l);

    for(int i = 0; i < l; ++i) {
        ans = ans * b % mod;

        llsi group = c.sum(ip[i]);
        llsi copy = i - c.sum(ip[i]);
        group = ksm(b, group);
        copy = ksm(b, copy);

        // std::cerr << "group = " << group << char(10);
        // std::cerr << "copy = " << copy << char(10);
        // std::cerr << "inv(v[" << ip[i] << "]) = " << inversion_calc(v[ip[i]]) << char(10);

        c.add(ip[i]);
        ans +=
            group * group % mod *
            (
                (copy * inversion_calc(v[ip[i]]) % mod) % mod +
                (inv2 * inv2 % mod * copy % mod * (copy - 1) % mod * b % mod * (b - 1) % mod)
            ) % mod;
        ans %= mod;
    }
    std::cout << ans << char(10);

    return 0;
}

M. Cartesian Trees

赛后没有题解看过的队的代码发现好像是个弱智题啊,不过比赛的时候也没时间想了

考虑先用单调栈求出每个位置 \(i\) 向左向右第一个小于它的位置 \(L_i,R_i\)

对于两段区间,如果这两段区间在 \(i-L_i\) 数组和 \(R_i-i\) 数组上的合法的对应部分完全相同的话,则构成的两棵树同构

所谓的合法的对应部分,即要满足对应的端点在给出的区间内,即对于一组 \((L_i,i)\) 来说,只有满足 \(l\le L_i\le i\le r\) 的询问它才需要产生贡献

维护子数组一致性可以用 Hash,最后用扫描线+树状数组离线处理即可

#include<cstdio>
#include<iostream>
#include<vector>
#include<set>
#define RI register int
#define CI const int&
using namespace std;
typedef unsigned long long u64;
const u64 seed1=233,seed2=163;
const int N=400005;
int n,q,a[N],L[N],R[N],stk[N],top,x,y; u64 pw1[N],pw2[N];
class Tree_Array
{
	private:
		u64 bit[N];
	public:
		#define lowbit(x) (x&-x)
		inline void add(RI x,const u64& y)
		{
			for (;x<=n;x+=lowbit(x)) bit[x]+=y;
		}
		inline u64 get(RI x,u64 ret=0)
		{
			for (;x;x-=lowbit(x)) ret+=bit[x]; return ret;
		}
		#undef lowbit
}BIT1,BIT2;
int main()
{
	scanf("%d",&n);
	for (RI i=1;i<=n;++i) scanf("%d",&a[i]);
	top=0; for (RI i=1;i<=n;++i)
	{
		while (top&&a[stk[top]]>a[i]) --top;
		L[i]=stk[top]; stk[++top]=i;
	}
	top=0; for (RI i=n;i>=1;--i)
	{
		while (top&&a[stk[top]]>a[i]) --top;
		R[i]=stk[top]; stk[++top]=i;
	}
//	for (RI i=1;i<=n;++i) printf("%d%c",L[i]," \n"[i==n]);
//	for (RI i=1;i<=n;++i) printf("%d%c",R[i]," \n"[i==n]);
	pw1[0]=pw2[0]=1ull;
	for (RI i=1;i<=n;++i) pw1[i]=pw1[i-1]*seed1,pw2[i]=pw2[i-1]*seed2;
	vector <pair <int,u64>> mdy1[N],mdy2[N];
	for (RI i=1;i<=n;++i)
	{
		if (L[i]) mdy1[i].push_back(make_pair(L[i],pw1[i]*(i-L[i])));
		if (R[i]) mdy2[R[i]].push_back(make_pair(i,pw2[i]*(R[i]-i)));
	}
	scanf("%d",&q);
	vector <int> ques[N];
	for (RI i=1;i<=q;++i)
	scanf("%d%d",&x,&y),ques[y].push_back(x);
	set <pair <u64,u64>> st;
	for (RI i=1;i<=n;++i)
	{
		for (auto [p,v]:mdy1[i]) BIT1.add(p,v);
		for (auto [p,v]:mdy2[i]) BIT2.add(p,v);
		for (auto j:ques[i])
		{
			u64 tmp1=(BIT1.get(i)-BIT1.get(j-1))*pw1[n-j];
			u64 tmp2=(BIT2.get(i)-BIT2.get(j-1))*pw2[n-j];
			st.insert(make_pair(tmp1,tmp2));
		}
	}
	return printf("%d",(int)st.size()),0;
}

Postscript

剩下的 A 题赛事感觉队友一直在做,到最后有个 \(O(n^2 \log n)\) 的神秘做法,不知道有没有写过

posted @ 2025-03-06 19:00  空気力学の詩  阅读(262)  评论(0)    收藏  举报