The 1st Universal Cup. Stage 22: Shaanxi

Preface

时隔一周之后难得开了场训练,然后犯了一堆弱智错误给自己搞红温了,最后感觉啥难题也没写就混着混着就结束了

赛后想补题经典找不到题解,只搜到哥哥的题解结果搞不懂细节还是写不来一点,直接开摆


D. Alice and Bob

首先博弈部分的结论很好猜,若第一次操作开头的数为 \(x\),则若 \(p_1\sim p_x\)\(\ge x\) 则先手必败

证明的话考虑满足上述条件后手一定能把 \(x\) 换回开头;否则先手可以把最小的数换到开头然后规约为上述情况

统计方案数的话就直接枚举 \(p_1\) 的值即可,式子很好推

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e7+5,mod=998244353;
int 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;
}
int main()
{
    scanf("%d",&n); init(n); int ans=0;
    for (RI i=1;i<=n;++i) (ans+=1LL*C(n-i,i-1)*fact[i-1]%mod*fact[n-i]%mod)%=mod;
    return printf("%d",ans),0;
}

G. Teleport

徐神的大手,用我闻所未闻的神秘建边淦飞了这个题

考虑一直用 teleport 操作的话会得到 \(n\) 条长度为 \(n\) 且互不相交的路径,路径上每个点可以跳到接下来的 \([1,k]\) 范围内的点,剩下四联通的走法就暴力连边

考虑优化 teleport 的建图,我们把长为 \(n\) 的点每 \(k\) 个分成一组,同时建两排辅助点,一排往后一排往前,但不连跨过组的边,大致结构如下:

(中间一排是原本的点,假设 \(n=9,k=3\),此时每个点都和后面 \([1,k]\) 范围的点连通)

然后就跑个最短路即可,复杂度 \(O(n^2)\)

#include <bits/stdc++.h>

int n, k;
int dis[3][5000][5000];
std::string ms[5000];
std::deque<std::tuple<short, short, short>> q;

inline short get_id(short x, short y) { return std::min(x, y) * 2 + (y > x); }

inline std::pair<short, short> get_pth_next(short x, short y, short p) {
    short fx = x, fy = y, bid = get_id(x, y);
    x += p >> 1, y += p >> 1;
    if(p & 1) std::swap(x, y), y++;
    if(x < n && y < n) return { x, y };
    if(fx < fy) std::swap(fx, fy), fy++;
    short dt = n - 1 - fx;
    fx += dt, fy += dt;
    if(get_id(fx, fy) / k == bid / k) return { -1, -1 };
    return { fx, fy };
}

inline std::pair<short, short> get_pth_prev(short x, short y, short p) {
    x -= p >> 1, y -= p >> 1;
    if(p & 1) std::swap(x, y), x--;
    return { x, y };
}

void update(short type, std::pair<short, short> ss, int d, int delta) {
    auto [nx, ny] = ss;
    if(nx < 0 || nx >= n || ny < 0 || ny >= n) return ;
    if(type == 0 && ms[nx][ny] == '*') return ;
    if(d + delta >= dis[type][nx][ny]) return ;
    dis[type][nx][ny] = d + delta;
    if(delta == 1) q.emplace_back(type, nx, ny);
    else           q.emplace_front(type, nx, ny);
    return ;
}

int hkr[10000];

std::ostream& operator <<(std::ostream &out, const std::pair<short, short> &p) {
    return out << p.first << " " << p.second;
}

int main() {
    std::ios::sync_with_stdio(false);
    memset(dis, 0x3f, sizeof(dis));
    std::cin >> n >> k;
    // for(int i = 0; i < n; ++i) for(int j = 0; j < n; ++j) std::cerr << get_id(i, j) << char(j == n - 1 ? 10 : 32);
    // std::cout << get_pth_next(0, 2, 3) << char(10);
    hkr[0] = 0;
    for(int i = 1; i < 10000; ++i) hkr[i] = (hkr[i - 1] + 1) % k;
    for(int i = 0; i < n; ++i) std::cin >> ms[i];
    for(int i = 0; i < n; ++i) for(int j = 0; j < i; ++j) std::swap(ms[i][j], ms[j][i]);
    q.push_back({0, 0, 0});
    if(ms[0][0] != '*') dis[0][0][0] = 0;
    while(q.size()) {
        auto [type, x, y] = q.front(); q.pop_front();
        short id = get_id(x, y);
        int d = dis[type][x][y];
        switch(type) {
            case 0:
                for(auto [dx, dy]: (short[4][2]){0, 1, 0, -1, 1, 0, -1, 0})
                    update(0, std::make_pair(x + dx, y + dy), d, 1);
                update(1, get_pth_next(x, y, 0), d, 1);
                update(2, get_pth_next(x, y, k), d, 1);
                break;
            case 1:
                if(hkr[id] + 1 != k) update(1, get_pth_next(x, y, 1), d, 0);
                update(0, std::make_pair(x, y), d, 0);
                break;
            case 2:
                if(hkr[id] != 0) update(2, get_pth_prev(x, y, 1), d, 0);
                update(0, std::make_pair(x, y), d, 0);
                break;
            default:
                abort();
        }
    }
    // for(int type = 0; type < 3; ++type) {
    //     for(int i = 0; i < n; ++i) for(int j = 0; j < n; ++j)
    //         std::cerr << (dis[type][i][j] >= 114514 ? "*" : std::format("{}", dis[type][i][j])) << char(j == n - 1 ? 10 : 32);
    //     std::cerr << "==================================\n";
    // }
    if(dis[0][n - 1][n - 1] > n * n * 3) std::cout << "-1\n";
    else std::cout << dis[0][n - 1][n - 1] << char(10);
    return 0;
}

H. Function

倒着推感觉不太可做,我们不妨定义一个新函数 \(g(x)\),初值为 \(g(x)=0\),转移为:

\[g(x)=1+\sum_{k=2}^{20210926} g(\lfloor\frac{x}{k}\rfloor) \]

经过证明打表我们发现 \(g(n)\) 和题目中 \(f(1)\) 的值是相同的,而这个式子一眼用除法分块优化,复杂度 \(O(n^\frac{3}{4})\)

#include<cstdio>
#include<iostream>
#include<cassert>
#include<unordered_map>
#define RI register int
#define CI const int&
using namespace std;
const int mod=998244353;
int n; unordered_map <int,int> g;
inline int G(CI n)
{
    if (g.count(n)) return g[n]; int ret=1;
    for (RI l=2,r;l<=n&&l<=20210926;l=r+1)
    {
        r=min(20210926,n/(n/l));
        (ret+=1LL*(r-l+1)*G(n/l)%mod)%=mod;
    }
    return g[n]=ret;
}
int main()
{
    scanf("%d",&n);
    /*for (int n=1;n<=1000;++n)
    {
        static int f[1005],g[1005];
        for (RI x=1000;x>=1;--x)
        {
            f[x]=1;
            for (RI k=2;k<=20210926;++k)
            {
                long long y=x*k;
                if (y>n) break;
                (f[x]+=f[y])%=mod;
            }
        }
        for (RI x=1;x<=1000;++x)
        {
            g[x]=1;
            for (RI k=2;k<=20210926;++k)
            {
                int y=x/k;
                if (y==0) break;
                (g[x]+=g[y])%=mod;
            }
        }
        assert(f[1]==g[n]);
        //printf("%d %d\n",f[1],g[n]);
    }*/
    return printf("%d",G(n)),0;
}

I. Digit

这种题一眼有效状态很少,直接大力对所有有用的状态记忆化搜索一下即可

稍微推一下期望的式子会发现因为是 DAG 所有转移的自环可以直接移项去除,实现的话注意一些常数

#include<bits/stdc++.h>
using namespace std;
#define int unsigned long long

const int MOD = 998244353;

void inc(int &x, int a) {if ((x+=a)>=MOD) x-=MOD;}
void mul(int &x, int a) {x=x*a%MOD;}

int powe(int x, int p) {
    int res=1;
    while (p>0) { if (p&1) mul(res, x); mul(x, x); p>>=1;}
    return res;
}

int n, lim;
unordered_map<int, int> mp;
int inv1[30], inv2[30][30];


vector<int> digit(int x) {
    vector<int> vec(10);
    while (x>0) {
        ++vec[x%10];
        x /= 10;
    }
    return vec;
}

int f(int x) {
    if (x > lim) return 0;
    if (mp.count(x)) return mp[x];

    vector<int> vec = digit(x);
    int res = 0;
    for (int i=0; i<=9; ++i) res += vec[i];

    // printf("x=%llu res=%llu\n", x, res);
    int ans = 0;
    for (int i=2; i<=10; ++i) if (vec[i-1]>0) {
        int tmp = 0;
        inc(tmp, (vec[i-1]*inv1[res]%MOD*f(x*i))%MOD);
        // printf("x=%llu i=%llu vec=%llu tmp=%llu\n", x, i, vec[i-1], tmp);
        inc(ans, tmp);
    }
    inc(ans, 1);
    if (vec[0] > 0) mul(ans, inv2[vec[0]][res]);
    return mp[x] = ans;
}

int solve() {
    cin >> n >> lim;
    mp.clear();
    // printf("n=%llu\n", n);
    int res = f(n);
    // for (auto [x, y] : mp) printf("(%llu %llu)", x, y); puts("");
    return res;
}

signed main() {
    // printf("%llu\n", powe(2, MOD-2));
    ios::sync_with_stdio(0); cin.tie(0);
    for (int i=1; i<=20; ++i) inv1[i] = powe(i, MOD-2);
    for (int i=1; i<=20; ++i) for (int j=i; j<=20; ++j) inv2[i][j] = powe((MOD+1 - i*powe(j, MOD-2)%MOD), MOD-2);
    int t; cin >> t; while (t--) cout << solve() << '\n';
    return 0;
}

J. Leaves

很一眼的题,令 \(f_{i,j}\) 表示处理了点 \(i\) 的子树,共用了 \(j\) 次交换操作能得到的最小的字典序

由树上背包的姿势只看这两维是 \(O(n^2)\) 的,但是由于合并两个子树时还有 \(O(n)\) 的开销,因此最后总复杂度 \(O(n^3)\)

实现的时候注意请看没用的状态不然会 MLE

#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=1005,INF=1e9+5;
int n,m,tp,l[N],r[N],a[N],sz[N]; vector <int> f[N][N/2];
inline void DFS(CI now=1)
{
    if (a[now])
    {
        f[now][0]={a[now]}; f[now][1]={INF};
        return;
    }
    int ls=l[now],rs=r[now]; sz[now]=1;
    DFS(ls); DFS(rs); sz[now]+=sz[ls]+sz[rs];
    for (RI i=0;i<=min(m,sz[now]);++i) f[now][i]={INF};
    auto merge=[&](vector <int>& A,vector <int>& B)
    {
        vector <int> vec=A;
        for (auto& x:B) vec.push_back(x);
        return vec;
    };
    for (RI p=0;p<=min(m,sz[ls]);++p)
    for (RI q=0;q<=min(m-p,sz[rs]);++q)
    {
        f[now][p+q]=min(f[now][p+q],merge(f[ls][p],f[rs][q]));
        if (p+q+1<=m) f[now][p+q+1]=min(f[now][p+q+1],merge(f[rs][q],f[ls][p]));
    }
    for (RI i=2;i<=min(m,sz[now]);++i) f[now][i]=min(f[now][i],f[now][i-2]);
    for (RI i=0;i<=min(m,sz[ls]);++i) f[ls][i].shrink_to_fit();
    for (RI i=0;i<=min(m,sz[rs]);++i) f[rs][i].shrink_to_fit();
}
int main()
{
    scanf("%d%d",&n,&m);
    for (RI i=1;i<=n;++i)
    {
        scanf("%d",&tp);
        if (tp==1) scanf("%d%d",&l[i],&r[i]);
        else scanf("%d",&a[i]);
    }
    DFS(); for (auto x:f[1][m]) printf("%d ",x);
    return 0;
}

K. water235

签到,先放一个对角线,然后多出来的维度每隔一个放即可

#include<bits/stdc++.h>
using namespace std;

const int N = 1e6+5;
bool flag;
int ans[N], c;

signed main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int n, m; cin >> n >> m;
    if (n>m) swap(n, m), flag=true;
    for (int i=0; i<n; ++i) ans[i*m+i]=1;
    for (int i=n; i<m; ++i) if ((i-n)%2==1) ans[i]=1;
    if (n<m) ans[m-1]=1;

    cout << n + (m-n+1)/2 << '\n';
    if (!flag) {
        for (int i=0; i<n; ++i) {
            for (int j=0; j<m; ++j) cout << ans[i*m+j] << ' ';
            cout << '\n';
        }
    } else {
        for (int j=0; j<m; ++j) {
            for (int i=0; i<n; ++i) cout << ans[i*m+j] << ' ';
            cout << '\n';
        }
    }
    return 0;
}

L. Square

神秘找规律题,手玩可以发现以下两个关键结论:

  • \([1],[2,3],[4,6],[7,10],\dots\) 每一段内的数增量相同,且段长依次加 \(1\)
  • 每个数距离其所在段右端点的距离保持恒定,例如 \(2\to 5\to 9\),距离所在段右端点的距离恒为 \(1\);

因此我们二分求出 \(x,y\) 的所在段以及距离右端点的距离后,分类讨论一下减一操作是在开头还是结束即可

#include<bits/stdc++.h>
using namespace std;
#define int long long

using pii = pair<int, int>;
#define ft first
#define sd second

pii find(int x) {
    auto calc = [&](int x) -> int{ return (1+x)*x/2;};
    int L=1, R=2e9+5;
    while (L<R) {
        int M=L+(R-L)/2;
        if (calc(M)>=x) R=M;
        else L=M+1;
    }
    return {L, calc(L)};
}

int solve() {
    int x, y; cin >> x >> y;
    if (x>y) return x-y;

    auto [v1, res1] = find(x);
    auto [v2, res2] = find(y);
    int pos = res2 - (res1-x);
    if (pos < y) {
        int ans = 0;
        res1 -= v1;
        --v1;
        return x-(res1-(res2-y)) + v2-v1;
    } else return v2 - v1 + pos - y;
}

signed main() {
    ios::sync_with_stdio(0); cin.tie(0);
    int t; cin >> t; while (t--) cout << solve() << '\n';
    return 0;
}

M. Delete the Tree

事到如今才发现我用了整个算竞生涯的点分治板子竟然有锅,不过只要不是在赛场上发现的都可以接受

这题思路其实很好想,就是求出点分树后按深度从下往上把同层的点一起处理即可

#include<cstdio>
#include<iostream>
#include<vector>
#include<cassert>
#define RI register int
#define CI const int&
using namespace std;
const int N=505;
int n,x,y,Rt; vector <int> v[N],nv[N],bkt[N];
namespace Point_Divide
{
    int rt,ots,sz[N],mx[N]; bool vis[N];
    inline void getrt(CI now=1,CI fa=0)
    {
        sz[now]=1; mx[now]=0;
        for (auto to:v[now]) if (to!=fa&&!vis[to])
        {
            getrt(to,now); sz[now]+=sz[to];
            mx[now]=max(mx[now],sz[to]);
        }
        if (mx[now]=max(mx[now],ots-sz[now]),mx[now]<mx[rt]) rt=now;
    }
    inline void solve(int now)
    {
        vis[now]=1; for (auto to:v[now]) if (!vis[to])
        mx[rt=0]=n,ots=sz[to],getrt(to,now),getrt(rt,0),nv[now].push_back(rt),solve(rt);
    }
    inline void divide(CI n)
    {
        mx[rt=0]=n; ots=n; getrt(); Rt=rt; solve(rt);
    }
}
inline void DFS(CI now=Rt,CI d=1)
{
    bkt[d].push_back(now);
    for (auto to:nv[now]) DFS(to,d+1);
}
int main()
{
    scanf("%d",&n);
    for (RI i=1;i<n;++i) scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
    Point_Divide::divide(n); DFS(); int mxd=-1;
    for (RI i=1;i<=n;++i) if (!bkt[i].empty()) mxd=i;
    printf("%d\n",mxd);
    assert(mxd<=10);
    for (RI i=mxd;i>=1;--i)
    {
        printf("%d ",bkt[i].size());
        for (auto x:bkt[i]) printf("%d ",x);
        putchar('\n');
    }
    return 0;
}

Postscript

感觉这场最大收获就是给板子捉了个虫,只要不在赛场上发现的板子出锅都是正收益啊

posted @ 2024-10-13 16:49  空気力学の詩  阅读(120)  评论(0)    收藏  举报