The 2023 ICPC Asia East Continent Final Contest

Preface

这场开局触发 QOJ 规则怪谈了,因为代码里有 char() 导致一直无法提交

被这个搞了大概 1h 后才开始交题,由于提前知道一些场外信息因此最后出了 8 题,放在现场都不敢想

赛后看现场榜才发现原来 CD 都过了不到十个,I 这个大码量题反而过了四十多个,感觉歪榜现象严重啊


B. Roman Master

首先考虑位数最短,在此基础上从前往后贪心地让每个数最小即可

倒着 DP 维护一个长度信息,从前往后贪心检验即可

#include<cstdio>
#include<iostream>
#include<string>
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,f[N]; string s;
inline int trs(const string& s)
{
    if (s=="I") return 1;
    if (s=="II") return 2;
    if (s=="III") return 3;
    if (s=="IV") return 4;
    if (s=="V") return 5;
    if (s=="VI") return 6;
    if (s=="VII") return 7;
    if (s=="VIII") return 8;
    return -1;
}
int main()
{
    ios::sync_with_stdio(0); cin.tie(0);
    for (cin>>t;t;--t)
    {
        cin>>s; n=(int)s.size();
        for (RI i=0;i<n;++i) f[i]=1e9;
        f[n]=0;
        for (RI i=n-1;i>=0;--i)
        {
            for (RI j=1;j<=4;++j)
            {
                if (i+j-1>=n) continue;
                if (trs(s.substr(i,j))==-1) continue;
                f[i]=min(f[i],f[i+j]+1);
            }
        }
        string ans=""; int p=0,cur=0;
        while (p<n)
        {
            int mn=9,len=-1;
            for (RI j=1;j<=4;++j)
            {
                if (p+j-1>=n) continue;
                int val=trs(s.substr(p,j));
                if (val==-1) continue;
                if (cur+1+f[p+j]!=f[0]) continue;
                if (val<mn) mn=val,len=j;
            }
            ans.push_back(mn+'0'); p+=len; ++cur;
        }
        cout<<ans<<'\n';
    }
    return 0;
}

C. Equal Sums

朴素的 DP 很好想,令 \(f_{i,j,k}\) 表示 \(\sum_{l=1}^i x_l-\sum_{l=1}^j y_l=k\) 的方案数,转移可以用前缀和优化,复杂度即为 DP 状态数 \(O(n^3\times 500)\)

但事实上我们可以强制第三维的值在 \([-500,500]\) 之间,不难证明任意一个合法的方案得到的过程一定可以由这样的状态表示

具体地,对于 \(f_{i,j,k}\),若 \(k\ge 0\) 则强制转移到 \(f_{i,j+1,k'}\);否则强制转移到 \(f_{i+1,j,k'}\),复杂度 \(O(n^2\times 500)\)

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=505,mod=998244353,S=500;
int n,m,lx[N],rx[N],ly[N],ry[N],f[N][N][N<<1];
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()
{
    scanf("%d%d",&n,&m);
    for (RI i=1;i<=n;++i)
    scanf("%d%d",&lx[i],&rx[i]);
    for (RI i=1;i<=m;++i)
    scanf("%d%d",&ly[i],&ry[i]);
    f[0][0][S+0]=1; f[0][0][S+1]=mod-1;
    for (RI i=0;i<=n;++i)
    for (RI j=0;j<=m;++j)
    {
        // if (i==n&&j==m)
        // {
        //     puts("f[n][m] = ");
        //     for (RI k=-S;k<=S;++k)
        //     printf("%d %d\n",k,f[i][j][S+k]);
        // }
        for (RI k=1;k<=2*S;++k)
        inc(f[i][j][k],f[i][j][k-1]);
        for (RI k=-S;k<=S;++k)
        {
            if (k<=0&&i+1<=n)
            {
                inc(f[i+1][j][S+k+lx[i+1]],f[i][j][S+k]);
                dec(f[i+1][j][S+k+rx[i+1]+1],f[i][j][S+k]);
            }
            if (k>0&&j+1<=m)
            {
                inc(f[i][j+1][S+k-ry[j+1]],f[i][j][S+k]);
                dec(f[i][j+1][S+k-ly[j+1]+1],f[i][j][S+k]);
            }
        }
    }
    for (RI i=1;i<=n;++i)
    for (RI j=1;j<=m;++j)
    printf("%d%c",f[i][j][S+0]," \n"[j==m]);
    return 0;
}

D. Random Permutation

神秘暴力题

考虑维护中位数的经典做法,初始时假设所有位置都是 \(1\),从小到大枚举每个数 \(x\),将其所在的位置 \(p\) 上的数改为 \(-1\)

此时所有包含了 \(p\) 且区间和为 \(0/-1\) 的区间对应的中位数就是 \(x\)

考虑如果我们确定了可能的左右边界 \(l_x,r_x\),则可以用桶做到 \(O(r_x-l_x)\) 的统计复杂度

直觉上考虑,当序列中 \(1/-1\) 的数量相差很大时,需要枚举的范围就很小;否则需要枚举的范围就很大

因此考虑令 \(t=|\frac{n}{2}-x|\),则左右边界的大小应该是和 \(t\) 有关的递减函数

在多次玄学调参后,发现令左右端点各扩展 \(10\times \frac{n^2}{t^2}\) 的步长可以通过此题,并且通过简单微积分计算可以发现此时 \(\sum_{x=1}^n r_x-l_x\)\(O(n\sqrt n)\) 级别的

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=300005,S=300000;
int n,pos[N],c[N],bkt[N<<1];
int main()
{
	scanf("%d",&n);
	for (RI i=1;i<=n;++i)
	{
		int x; scanf("%d",&x);
		pos[x]=i; c[i]=1;
	}
	long long ans=0;
	for (RI x=1;x<=n;++x)
	{
		int t=abs(n/2-x),i=pos[x];
		c[i]=-1; long long len=t>0?((1LL*n*n)/(1LL*t*t)):n,sum;
		int llen=min((long long)n,10LL*len);
		int l=max(i-llen,1),r=min(n,i+llen);
		if (n<=5000) l=1,r=n;
		sum=0; for (RI j=i;j>=l;--j)
		{
			sum+=c[j];
			++bkt[S+sum];
		}
		long long cur=0;
		cur+=bkt[S]+bkt[S-1];
		sum=0; for (RI j=i+1;j<=r;++j)
		{
			sum+=c[j];
			cur+=bkt[S-sum]+bkt[S-sum-1];
		}
		sum=0; for (RI j=i;j>=l;--j)
		{
			sum+=c[j];
			--bkt[S+sum];
		}
		ans+=1LL*x*cur;
	}
	return printf("%lld",ans),0;
}

E. Colorful Graph

分类讨论题

  • \(d=1\) 时,直接连成完全图;
  • \(d=2\) 时,除了每种颜色内部的边都可以直接相连;
  • \(d\ge 3\) 时,手玩发现此时最优的构造法就是找若干个颜色不同的 clique,clique 内部连成完全图;
#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=500005;
int t,n,d,a[N];
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d%d",&n,&d);
        for (RI i=1;i<=n;++i) scanf("%d",&a[i]);
        sort(a+1,a+n+1);
        auto C2=[&](CI x)
        {
            return 1LL*x*(x-1)/2LL;
        };
        if (d==1)
        {
            long long sum=0;
            for (RI i=1;i<=n;++i) sum+=a[i];
            printf("%lld\n",C2(sum));
            continue;
        }
        if (d==2)
        {
            long long ans=0,sum=0;
            for (RI i=1;i<=n;++i) sum+=a[i];
            for (RI i=1;i<=n;++i) ans+=1LL*a[i]*(sum-a[i]);
            printf("%lld\n",ans/2LL);
            continue;
        }
        long long ans=0;
        for (RI i=n;i>=1;--i) a[i]-=a[i-1];
        for (RI i=1;i<=n;++i) ans+=1LL*a[i]*C2(n-i+1);
        printf("%lld\n",ans);
    }
    return 0;
}

F. Dot Product

观察一下会发现最后的序列中 \(i\) 一定在 \(i\) 或者 \(i+1\) 的位置上

因此最后合法的序列一定是在 \(1,2,\dots,n\) 的序列基础上,交换若干对不相交的相邻元素 \((i,i+1)\) 得到的

先求出原序列的逆序对,并统计每对 \((i,i+1)\) 初始位置的先后关系,如果 \(i+1\)\(i\) 之前则可以在这里减少一次交换,用 DP 统计出最多能减少的贡献值即可

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

const int N = 5e5+5;
int n, A[N], pp[N], dp[N];
int fen[N];
int lb(int x){return x&(-x);}
void upd(int pos, int val) {
    for (int i=pos; i<=n; i+=lb(i)) fen[i] += val;
}
int qry(int pos) {
    int res = 0;
    for (int i=pos; i>0; i-=lb(i)) res += fen[i];
    return res;
}

int solve() {
    cin >> n;
    for (int i=1; i<=n; ++i) fen[i] = 0;

    for (int i=1; i<=n; ++i) {
        cin >> A[i];
        pp[A[i]] = i;
    }
    int ans = 0;
    for (int i=n; i>0; --i) {
        ans += qry(A[i]);
        upd(A[i], 1);
    }
    // printf("ans=%d\n", ans);
    for (int i=1; i<=n+1; ++i) dp[i] = 0;
    for (int i=n-1; i>0; --i) {
        int res = (pp[i] > pp[i+1]);
        dp[i] = max(dp[i+1], res + dp[i+2]);
    }
    // printf("dp:"); for (int i=1; i<=n; ++i) printf("%d ", dp[i]); puts("");
    return ans - max(dp[1], dp[2]);
}

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

I. Balance

徐神声称会了此题,但因为赛时时间所限写不完力

做法大致就是点双缩点后在树上 DP/启发式合并,反正我是一点也不想写


J. Travel 2

题都没看就被徐神秒了

#include <bits/stdc++.h>

void answer(const std::set<std::pair<int, int>> ans) {
    std::cout << "!";

    for(auto [l, r]: ans) std::cout << " " << l + 1 << " " << r + 1;
    std::cout << std::endl;
    std::string response;
    std::cin >> response;
    if(response != "Correct") exit(0);
}

void work() {
    int n = 2500, m;

    std::vector<bool> vis(n, false);
    std::vector<int> od(n, 0), pa(n, 0), pao(n, -1), hkr(n, 0);
    std::set<std::pair<int, int>> ans;
    std::vector<std::vector<std::pair<int, int>>> ch(n);

    int cur;
    std::cin >> cur;
    std::cin >> od[--cur];
    vis[cur] = true;

    auto walk = [&](int d) {
        std::cout << "> " << d + 1 << std::endl;

        int des, des_od;
        std::cin >> des >> des_od;
        des -= 1;
        od[des] = des_od;
        
        // std::cerr << "walk " << cur + 1 << " --(" << d + 1 << ")--> " << des + 1 << std::endl;

        if(des == pa[cur]) pao[cur] = d;
        if(des > cur) ans.insert({cur, des});
        else          ans.insert({des, cur});

        if(!vis[des]) pa[des] = cur, ch[cur].emplace_back(des, d), vis[des] = 1;
        cur = des;
    };

    auto dfs = [&](auto self) -> void {
        while(hkr[cur] < od[cur]) walk(hkr[cur]++);

        const auto &_ch = ch[cur];
        for(auto [ch, out]: _ch) {
            walk(out);
            self(self);
            walk(pao[ch]);
        }
    };

    dfs(dfs);
    answer(ans);
    return ;
}

int main() {
    std::ios::sync_with_stdio(false);

    int T; std::cin >> T; while(T--) work();
    return 0;
}


K. Best Carry Player 4

首先考虑把所有的 \(i,m-1-i\) 配对起来,记这样的 pair 有 \(k\) 对,那么只要后面有一个进位就可以产生 \(k\) 的贡献

在剩下的数中,可以用双指针求出剩下的数中最多有多少对能直接进位的,显然如果这个数不为零就已经得到了答案

考虑要么拆掉前面配对的数中 \(a_i/b_i\) 最大的一个去和剩下的数凑一个 trigger,如果能凑出来的话则答案为 \(k\)

否则考虑把最大的 \(a_i/b_i\) 都拆出来看能否构成 trigger,如果能的话答案为 \(k-1\);否则答案为 \(0\)

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

const int N = 5e5+5;

int m;

int calc(vector<int> &A, vector<int> &B) {
    int sumA=0, sumB=0;
    int res = 0;
    for (int i=1, j=m-1; i<m; ++i) {
        int bd = m-i;
        while (j >= bd && A[i] > 0) {
            if (0 == B[j]) {
                --j;
                continue;
            }
            int mn = min(A[i], B[j]);
            res += mn;
            A[i] -= mn, B[j] -= mn;
        }
    }
    return res;
}

int solve() {
    cin >> m;
    vector<int> A(m), B(m), aa(m), bb(m);
    int sumA = 0, sumB = 0;
    for (int i=0; i<m; ++i) cin >> A[i], sumA += A[i];
    for (int i=0; i<m; ++i) cin >> B[i], sumB += B[i];
    if (sumA < sumB) A[0] += sumB - sumA;
    if (sumA > sumB) B[0] += sumA - sumB;
    int ans = 0;
    int mxpa = -1, mxpb = -1;
    for (int i=0; i<m; ++i) {
        int mn = min(A[i], B[m-1-i]);
        aa[i] = A[i] - mn;
        bb[m-1-i] = B[m-1-i] - mn;
        if (mn > 0) {
            mxpa = max(mxpa, i);
            mxpb = max(mxpb, m-1-i);
        }
        ans += mn;
    }

    // int mxa = max_element(aa.begin(), aa.end()) - aa.begin();
    // int mxb = max_element(bb.begin(), bb.end()) - bb.begin();
    int mxa = -1, mxb = -1;
    for (int i=m-1; i>=0; --i) {
        if (aa[i] > 0 && -1 == mxa) {
            mxa = i;
        }
        if (bb[i] > 0 && -1 == mxb) {
            mxb = i;
        }
    }

    int res = calc(aa, bb);
    if (res > 0) return ans + res;
    if (mxpb >= 0 && mxa >= 0 && mxa + mxpb >= m) return ans; 
    if (mxpa >= 0 && mxb >= 0 && mxb + mxpa >= m) return ans; 
    if (mxpa + mxpb >= m) return ans -1;
    return 0;
}

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

L. Binary vs Ternary

龟龟徐神的构造实力

大致的思路就是先把原串变为 \(10\),再逐步扩展为目标串

前者只需要从后往前重复 \(11\to 100\to 10\) 的操作即可

后者可以注意到 \(10\to 11\to 100\) 可以在后面生成一个 \(0\)\(10\to 11\to 100\to 110\) 可以在后面生成一个 \(1\),依照此法即可生成目标串

#include <bits/stdc++.h>

void work() {
    std::string a, b;
    std::cin >> a >> b;
    int n = a.size(), m = b.size();

    if(n == 1 && m == 1) return std::cout << "0\n", void(0);
    if(n == 1 || m == 1) return std::cout << "-1\n", void(0);

    std::vector<std::pair<int, int>> op;

    for(int i = 1; i < n; ++i) if(a[i] == '0') {
        op.emplace_back(i - 1, i);
        a[i] = '1';
    }

    // 11 -> 100 -> 10
    op.emplace_back(n - 2, n - 1);
    op.emplace_back(n - 1, n);

    // 110 -> 1000 -> 10
    for(int i = n - 3; i >= 0; --i) {
        op.emplace_back(i, i + 1);
        op.emplace_back(i + 1, i + 3);
    }

    // op.emplace_back(-2, -2);

    int sta = 0;
    if(b.back() == '1') op.emplace_back(0, 1), sta = 1;
    for(int i = m - 2; i > 0; --i) {
        if(sta == 0) {
            if(b[i] == '0')
                op.emplace_back(0, 1),
                op.emplace_back(0, 1),
                sta = 0;
            else
                op.emplace_back(0, 1),
                op.emplace_back(0, 1),
                op.emplace_back(0, 1),
                sta = 1;
        } else {
            if(b[i] == '0')
                op.emplace_back(0, 1),
                op.emplace_back(0, 2),
                op.emplace_back(1, 2),
                sta = 0;
            else
                op.emplace_back(0, 1),
                op.emplace_back(0, 1),
                op.emplace_back(1, 2),
                sta = 1;
        }
    }

    std::cout << op.size() << '\n';
    for(auto [l, r]: op) std::cout << l + 1 << " " << r + 1 << '\n';
    return ;
}

int main() {
    std::ios::sync_with_stdio(false);

    int T; std::cin >> T; while(T--) work();
    return 0;
}


Postscript

感觉最近神一场鬼一场的

posted @ 2025-05-04 17:35  空気力学の詩  阅读(141)  评论(0)    收藏  举报