The 2023 ICPC Asia Jinan Regional Contest (The 2nd Universal Cup. Stage 17: Jinan)

Preface

趁着开学前最后一天再凑一场训练,今天这场手感不错前面的题都是一遍过

最后靠着前期的手速7题苟进Au区,后面90min徐神B题没有Rush出来,思路啥都是对的就是一点细节没写好


A. Many Many Heads

首先发现我们可以将所有相邻的同类型括号设为一对,这样一定能得出一个合法的串

考虑将这个括号序列分层处理,最外层可能有若干个子括号对,不难发现当其中出现某种括号的对数\(\ge 2\)就一定有多解

因为这些没有嵌套关系的括号之间互不影响,只要有\(\ge 4\)个位置上有同类型的括号的话那么必然存在大于一种放法

#include <bits/stdc++.h>

std::string s;

int type(int c) { return c == '(' || c == ')'; }

bool check(int &i, int pre) {
    if(i == s.size()) return true;
    if(pre >> type(s[i]) & 1) return false;
    pre |= 1 << type(s[i]);
    int t = i;
    while(type(s[i + 1]) != type(s[i])) i += 1;
    int j = i + 1;
    while(i >= t) {
        if(type(s[i]) != type(s[j])) return false;
        i--; j++;
    }
    return check(i = j, pre);
}

int main() {
    std::ios::sync_with_stdio(false);
    int T; std::cin >> T; while(T--) {
        std::cin >> s;
        int i = 0;
        std::cout << (check(i, 0) ? "Yes\n" : "No\n");
    }
    return 0;
}

B. Graph Partitioning 2

这题大致思路徐神都想到了,但最后没写出来有点可惜

考虑根号分治,当\(k\le \sqrt n\)时,直接记录包含当前子树根节点的连通块大小进行树上背包DP即可

\(k>\sqrt n\),仍然考虑树上背包,但改为记当前子树内分出的大小为\(k\)的连通块个数,并记录有\(0/1\)个包含当前子树根的大小为\(k+1\)的连通块即可转移

总复杂度\(O(n\sqrt n)\),但代码细节比较多,坐等徐神补题


C. Turn on the Light 2

好难的题,做不来捏、


D. Largest Digit

签到,不难发现当某个区间的大小\(\ge 10\)时则和的个位数一定可以为\(9\);否则直接暴力判断即可

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t,la,ra,lb,rb;
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d%d%d%d",&la,&ra,&lb,&rb);
        if (ra-la+1>=10||rb-lb+1>=10) { puts("9"); continue; }
        auto calc=[&](int x)
        {
            int ret=0; while (x>0) ret=max(ret,x%10),x/=10; return ret;
        };
        int ans=0; for (RI x=la;x<=ra;++x) for (RI y=lb;y<=rb;++y) ans=max(ans,calc(x+y));
        printf("%d\n",ans);
    }
    return 0;
}

E. I Just Want... One More...

刚开始想简单了但稀里糊涂地过了样例,后面下去反思了下才发现问题过了这个题,差点酿成大祸

首先考虑由源点向左部点连边,右部点向汇点连边,先跑一个最大流出来

考虑如果要新加一条边\(x\to y\),则需要在残量网络上,存在源点到\(x\)的路径以及\(y\)到汇点的路径

根据残量网络的性质,前者等价于从源点开始走剩余容量为\(1\)的边能到达的左部点数;后者等价于从汇点开始走剩余容量为\(0\)的边能到达的右部点数

#include<cstdio>
#include<iostream>
#include<cstring>
#include<queue>
#include<cctype>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005,INF=1e9;
int T,n,m,x,y;
class FileInputOutput
{
    private:
        static const int S=1<<21;
        #define tc() (A==B&&(B=(A=Fin)+fread(Fin,1,S,stdin),A==B)?EOF:*A++)
        char Fin[S],*A,*B;
    public:
        inline void read(int& x)
        {
            x=0; char ch; while (!isdigit(ch=tc()));
            while (x=(x<<3)+(x<<1)+(ch&15),isdigit(ch=tc()));
        }
}F;
namespace Network_Flow
{
    const int NN=200005,MM=1e7+5;
    struct edge
    {
        int to,nxt,v;
    }e[MM<<1]; int cnt=1,head[NN],cur[NN],dep[NN],s,t;
    inline void addedge(CI x,CI y,CI z)
    {
        e[++cnt]=(edge){y,head[x],z}; head[x]=cnt;
        e[++cnt]=(edge){x,head[y],0}; head[y]=cnt;
    }
    inline void clear(void)
    {
        memset(head,0,(t+1)*sizeof(int)); cnt=1;
    }
    #define to e[i].to
    inline bool BFS(void)
    {
        memset(dep,0,(t+1)*sizeof(int)); dep[s]=1;
        queue <int> q; q.push(s);
        while (!q.empty())
        {
            int now=q.front(); q.pop();
            for (RI i=head[now];i;i=e[i].nxt)
            if (e[i].v&&!dep[to]) dep[to]=dep[now]+1,q.push(to);
        }
        return dep[t];
    }
    inline int DFS(CI now,CI tar,int dis)
    {
        if (now==tar) return dis; int ret=0;
        for (RI& i=cur[now];i&&dis;i=e[i].nxt)
        if (e[i].v&&dep[to]==dep[now]+1)
        {
            int dist=DFS(to,tar,min(dis,e[i].v));
            if (!dist) dep[to]=INF;
            dis-=dist; ret+=dist; e[i].v-=dist; e[i^1].v+=dist;
            if (!dis) return ret;
        }
        if (!ret) dep[now]=0; return ret;
    }
    inline int Dinic(int ret=0)
    {
        while (BFS()) memcpy(cur,head,(t+1)*sizeof(int)),ret+=DFS(s,t,INF); return ret;
    }
};
using namespace Network_Flow;
inline int travel(CI st,CI L,CI R,CI tp)
{
    static int vis[N]; memset(vis,0,(t+1)*sizeof(int));
    queue <int> q; q.push(st); vis[st]=1; int ret=0;
    while (!q.empty())
    {
        int now=q.front(); q.pop(); ret+=(L<=now&&now<=R);
        for (RI i=head[now];i;i=e[i].nxt)
        if (e[i].v==tp) if (!vis[to]) vis[to]=1,q.push(to);
    }
    return ret;
}
#undef to
int main()
{
	//freopen("E.in","r",stdin);
    for (F.read(T);T;--T)
    {
        RI i,j; for (F.read(n),F.read(m),i=1;i<=m;++i)
        F.read(x),F.read(y),addedge(x,y+n,1);
        for (s=2*n+1,t=2*n+2,i=1;i<=n;++i)
        addedge(s,i,1),addedge(i+n,t,1);
        Dinic(); printf("%lld\n",1LL*travel(s,1,n,1)*travel(t,n+1,2*n,0)); clear();
    }
    return 0;
}

F. Say Hello to the Future

啥玩意做不来一点


G. Gifts from Knowledge

很经典的一个题,首先考虑对于两个位置相对的列\(i,c-i+1\),统计这两列中\(1\)的个数

显然若存在\(\ge 3\)\(1\),则不论怎么操作这两列最后一定存在某列不合法;若存在\(\le 1\)\(1\),则不论怎么操作都一定合法

现在考虑存在\(2\)\(1\)的情况,不妨设这两个\(1\)在第\(x,y\)行(允许\(x=y\)),则:

  • 若两个\(1\)在同一列,则第\(x\)行和第\(y\)行有且仅有一行要翻转
  • 若两个\(1\)在不同列,则第\(x\)行和第\(y\)行要么都翻转,要么都不翻转

这是一个经典问题,可以用黑白染色或者拆点+并查集解决,最后方案数就是\(2\)的连通块数量次幂

#include <bits/stdc++.h>

using llsi = long long signed int;
constexpr llsi mod = 1'000'000'007;

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;
}

std::vector<int> fa;
inline int father(int i) {
    if(fa[i] == i) return i;
    return fa[i] = father(fa[i]);
}

void unionn(int a, int b) {
    a = father(a); b = father(b);
    if(a == b) return ;
    fa[b] = a;
}

int work() {
    int n, m; std::cin >> n >> m;
    std::vector<std::string> b(n);
    for(auto &b: b) std::cin >> b;
    fa.resize(2 * n);
    for(int i = 0; i < 2 * n; ++i) fa[i] = i;
    int Tc = 0;
    for(int i = 0; i < m; ++i) {
        int c = 0;
        for(int j = 0; j < n; ++j) c += (b[j][i] == '1');
        if(c > 2) return 0;
        Tc += c; if(Tc > m) return 0;
    }
    for(int l = 0, r = m - 1; l <= r; ++l, --r) {
        std::vector<int> L, R;
        for(int s = 0; s < n; ++s) if(b[s][l] == '1') L.push_back(s);
        for(int s = 0; s < n; ++s) if(b[s][r] == '1') R.push_back(s);
        if(L.size() == 2) unionn(L[0], L[1] + n), unionn(L[0] + n, L[1]);
        if(R.size() == 2) unionn(R[0], R[1] + n), unionn(R[0] + n, R[1]);
        for(auto L: L) for(auto R: R) unionn(L, R), unionn(L + n, R + n);        
    }
    for(int i = 0; i < n; ++i) if(father(i) == father(i + n)) return 0;
    int s = 0;
    for(int i = 0; i < 2 * n; ++i) s += (father(i) == i);
    return ksm(2, s / 2);
}

int main() {
    std::ios::sync_with_stdio(false);
    int T; std::cin >> T; while(T--) std::cout << work() << char(10);
    return 0;
}

H. Basic Substring Structure

看题面像是个字符串就没敢想,后面发现好像是个DS,不管了反正我摆了


I. Strange Sorting

小清新构造题

考虑从前往后还原每个位置,若\(a_i\ne i\)则选定\(i\)作为左端点

不妨找到数\(i\)所在的位置\(j\),显然\(j>i\),同时找到数\(i+1\)所在的位置\(k\)

  • \(i<k<j\),则我们操作区间\([i,j]\),那么可以至少同时复原\(i,i+1\)
  • \(i<j<k\),那此时\(a_i>a_k\)必然成立,那么我们操作区间\([i,k]\)亦可以同时复原\(i,i+1\)

综上,每次我们可以至少复原\(2\)个数,因此最后操作次数就是\(\frac{n}{2}\)

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

using pii = pair<int, int>;
const int N = 105;
int t, n, A[N];
signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> t;
    while (t--){
        cin >> n;
        for (int i=1; i<=n; ++i) cin >> A[i];
        vector<pii> ans;
        int cur=1;
        while (cur<=n){
            if (A[cur]==cur){++cur; continue;}
            int posa, posb;
            for (int i=cur; i<=n; ++i){
                if (A[i]==cur) posa=i;
                if (A[i]==cur+1) posb=i;
            }
            if (posa>posb) ans.push_back(make_pair(cur, posa)), sort(A+cur, A+posa+1);
            else ans.push_back(make_pair(cur, posb)), sort(A+cur, A+posb+1);
            cur+=2;
        }
        cout << ans.size() << '\n';
        for (auto [a, b] : ans) cout << a << ' ' << b <<'\n';
    }
    return 0;
}

J. Computational Intelligence

最害怕的一题,超级恶心的分类讨论积分题,像我这种高数渣直接一眼寄


K. Rainbow Subarray

刚开始思路想偏了一直在想在差分数组上怎么做,后面祁神直接修改了前提马上就会了这个题

首先不妨将每个\(a_i\)减去\(i\),那么问题转化为找一段区间将其中所有数变成相同的

不难发现对于一个确定的区间,显然将所有数都变成其中位数是最优的

动态求一个集合的中位数是个经典问题,我们拿两个multiset分别维护小于等于中位数的数以及大于等于中位数的数,并保证二者的大小差\(\le 1\)即可

同时要计算贡献的话只要顺带记一下每个multiset内数的和即可,最后在外面套一个尺取法即可\(O(n\log n)\)求解本题

#include<cstdio>
#include<iostream>
#include<set>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=500005;
int t,n,k,a[N];
class Medium_Solver
{
    private:
        multiset <int> A,B; int SA,SB;
    public:
        inline void init(void)
        {
            A.clear(); B.clear(); SA=SB=0;
        }
        inline void insert(CI x)
        {
            if (B.empty())
            {
                B.insert(x); SB+=x; return;
            }
            int mid=*B.begin();
            if (x>=mid)
            {
                B.insert(x); SB+=x;
                if (A.size()<B.size()-1)
                {
                    auto it=B.begin(); SB-=*it; SA+=*it;
                    A.insert(*it); B.erase(it);
                }
            } else
            {
                A.insert(x); SA+=x;
                if (A.size()>B.size())
                {
                    auto it=(--A.end()); SA-=*it; SB+=*it;
                    B.insert(*it); A.erase(it);
                }
            }
        }
        inline void remove(CI x)
        {
            int mid=*B.begin();
            if (x>=mid)
            {
                B.erase(B.find(x)); SB-=x;
                if (A.size()>B.size())
                {
                    auto it=(--A.end()); SA-=*it; SB+=*it;
                    B.insert(*it); A.erase(it);
                }
            } else
            {
                A.erase(A.find(x)); SA-=x;
                if (A.size()<B.size()-1)
                {
                    auto it=B.begin(); SB-=*it; SA+=*it;
                    A.insert(*it); B.erase(it);
                }
            }
        }
        inline int calc(void)
        {
            int mid=*B.begin();
            return (mid*A.size()-SA)+(SB-mid*B.size());
        }
        inline void DEBUG(void)
        {
            for (auto x:A) printf("%lld ",x); printf("SA= %lld\n",SA);
            for (auto x:B) printf("%lld ",x); printf("SB= %lld\n",SB);
        }
}S;
signed main()
{
    ios::sync_with_stdio(false); cin.tie(0);
    for (cin>>t;t;--t)
    {
        RI i,j; for (cin>>n>>k,i=1;i<=n;++i) cin>>a[i],a[i]-=i;
        int ans=0; for (S.init(),i=1,j=0;i<=n;++i)
        {
            while (j<n)
            {
                S.insert(a[++j]);
                //printf("i=%lld j=%lld ans=%lld\n",i,j,S.calc());
                //S.DEBUG();
                if (S.calc()>k) { S.remove(a[j--]); break; }
            }
            ans=max(ans,j-i+1); S.remove(a[i]);
        }
        cout<<ans<<'\n';
    }
    return 0;
}

L. Ticket to Ride

好诡异的题,做不来捏


M. Almost Convex

洒洒水几何题,被祁神一眼秒

直接求出原点集的凸包,考虑把凸包的某个角凹进去是否能得到更优的答案

枚举凹进去的顶点\(a\),转化一下就变成要求找所有三角形\(\triangle abc\)满足其内部没有其它点(其中\(a,b\)为凸包的某条边)

然后这个东西是个很典的问题,转化到夹角上就是个二维偏序问题,直接那单调栈维护一下即可

复杂度\(O(n^2\log n)\)

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

using pii = pair<int, int>;

struct Pt{
    int x, y;
    Pt operator-(Pt b)const{return Pt{x-b.x, y-b.y};}
    int crs(Pt b)const{return x*b.y-y*b.x;}
    bool operator<(Pt b)const{return x!=b.x ? x<b.x : y<b.y;}
};
int cross(Pt p, Pt a, Pt b){return (a-p).crs(b-p);}

const int N = 2e3+5;
int n;
vector<int> CH;
Pt pt[N];
bool inCH[N];

void makeCH(vector<int> &vec){
    int sz = vec.size();
    sort(vec.begin(), vec.end(), [&](int a, int b){return pt[a]<pt[b];});
    // printf("vec:"); for (int xx : vec) printf("(%lld %lld)", pt[xx].x, pt[xx].y);
    vector<int> stk(sz+5); int top=-1;
    stk[++top] = vec[0];
    for (int i=1; i<sz; ++i){
        while (top>0 && cross(pt[stk[top-1]], pt[stk[top]], pt[vec[i]])>=0) --top;
        stk[++top]=vec[i];
    }
    int dn = top;
    for (int i=sz-2; i>=0; --i){
        while (top>dn && cross(pt[stk[top-1]], pt[stk[top]], pt[vec[i]])>=0) --top;
        stk[++top]=vec[i];
    }
    vec.clear(); vec.insert(vec.begin(), stk.begin(), stk.begin()+top);
}

int solve(int x, vector<int> ots){
    int sz=ots.size();
    sort(ots.begin(), ots.end(), [&](int a, int b){return cross(pt[CH[x]], pt[a], pt[b])>0;});
    vector<int> stk(sz+5); int top=0;
    for (int xx : ots){
        while (top>00 && cross(pt[CH[x+1]], pt[xx], pt[stk[top]])>=0) --top;
        stk[++top]=xx;
    }
    return top;
}

signed main(){
    ios::sync_with_stdio(0); cin.tie(0);
    cin >> n;
    CH.resize(n);
    for (int i=0; i<n; ++i){
        cin >> pt[i].x >> pt[i].y;
        CH[i]=i;
    }
    makeCH(CH);
    // printf("CH:"); for (int x : CH) printf("(%lld %lld)", pt[x].x, pt[x].y); puts("");
    vector<int> ots;
    for (int x : CH) inCH[x]=true;
    for (int i=0; i<n; ++i) if (!inCH[i]) ots.push_back(i);

    int sz=CH.size();
    CH.push_back(CH[0]);
    int ans=0;
    for (int i=0; i<sz; ++i){
        ans += solve(i, ots);
        // printf("i=%lld ans=%lld\n", i, ans);
    }
    cout << ans+1 << '\n';
    return 0;
}

Postscript

唉明天就要开学力,美好生活已经结束力

posted @ 2024-02-25 20:47  空気力学の詩  阅读(677)  评论(0编辑  收藏  举报