ICPC2023香港站题解(A D E H I J)

本场金牌为超低罚时六题,稳拿金牌需要做出第七题。

但是我只会六题,这里是前六题的题解。

ICPC2023香港站

J:

签到但不是完全签到,需要讲。

首先每个位置只会走一次,所以让 \(a_i\) 加一的操作只会在第一次到达某个位置时连续施行。

\(a_i\) 加一再跳转需要花费一个时间,让 \(a_i\) 加二再跳转需要花费两个时间,你可以理解成先走到 \(i+a_i\) 的位置,再花费 1 的时间往后走一个位置,也可以花费 2 的时间往后走两个位置。所以我们把每一个位置往下一个位置连花费为1的边就好了。

但这个连边是需要一个前提的,我们得能够先走到 \(i\) 的位置,这些向后连的边才有意义。比如终点是1,但是 \(a_0=2\),你是不能一步到达的。

解决方案很简单,不要把0当初始点,而是把 \(a_0\) 当初始点,因为走到终点是至少需要跳这一步的。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;

ll T;
ll n,X;
ll ans,a[N];
struct E{
    ll to,we;
};
vector <E> e[N];
struct D{
    ll id,di;
};
bool operator < (D A,D B) { return A.di > B.di; }
priority_queue <D> q;
ll dis[N];

inline ll read() {
    ll sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

inline void add(ll u,ll v,ll z) {
    e[u].push_back({v,z});
}

void DIJ() {
    memset(dis,0x3f,sizeof(dis));
    q.push({a[0],0}); dis[a[0]] = 0;
    while(!q.empty()) {
        D now = q.top(); q.pop();
        ll u = now.id;
        if(dis[u]!=now.di) continue;
        FOR() {
            ll v = e[u][i].to, w = e[u][i].we;
            if(dis[u] + w < dis[v]) {
                dis[v] = dis[u] + w;
                q.push({v,dis[v]});
            }
        }
    }
}

int main() {
	n = read(); X = read();
    for(ll i=0;i<n;i++) a[i] = read(), add(i,(a[i]+i)%n,1);
    for(ll i=0;i<n;i++) add(i,(i+1)%n,1);
    DIJ();
    cout<<dis[X]+1;
    return 0;
}

A:

一道很像网络流的矩阵选数题。题面中的 NP-complete 把我队友给误导了,其实解法很简单。

和之前一道类似的矩阵构造题很像,先全部改成一致颜色,然后只需要考虑一个方向。

这题的解法也是类似,先把所有的1改成0,之后修改任意位置其实都是加一。为了同时满足行和列的要求,有一个贪心的选数方法,比较经典。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const int N=501010;
const int qwq=303030;
const int inf=0x3f3f3f3f;

int T;
int n,m;
int a[4040][4040],b[4040][4040];
char s[N];
int hang[N];
struct E{
    int id,zhi;
}lie[N];
inline bool cmp(E A,E B) { return A.zhi < B.zhi; }

inline int read() {
    int sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

int main() {
	n = read();
    for(int i=1;i<=n;i++) lie[i].id = i;
    for(int i=1;i<=n;i++) {
        scanf("%s",s+1);
        for(int j=1;j<=n;j++) {
            if(s[j]=='-') a[i][j] = -1;
            else {
                a[i][j] = 1;
                hang[i]--;
                lie[j].zhi--;
            }
        }
    }
    for(int i=1;i<=n;i++) hang[i] += read();
    for(int i=1;i<=n;i++) lie[i].zhi += read();
    for(int i=1;i<=n;i++) {
        if(hang[i]>0) { cout<<"No"; return 0; }
        sort(lie+1,lie+n+1,cmp);
        for(int j=1;j<=(-hang[i]);j++) lie[j].zhi++, b[i][lie[j].id] = 1;
    }
    // bool ke = 1;
    for(int i=1;i<=n;i++) if(lie[i].zhi!=0) { cout<<"No"; return 0; }
    cout<<"Yes\n";
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=n;j++) {
            if(a[i][j]==1) {
                if(b[i][j]) cout<<0;
                else cout<<1;
            }
            else {
                if(b[i][j]) cout<<1;
                else cout<<0;
            }
        }
        cout<<endl;
    }
    return 0;
}

I:

当你的重置能力CD好了之后,你有两个选择:一是等你的普通攻击CD,CD好了之后先攻击,再重置,然后立即攻击;二是马上重置,然后攻击。

其他的选择,你手玩一下会发现是愚昧的。

而这两个选择结果都是相同的,都会使得两个技能重新开始冷却(其实就和重新开始一个情况了)。

这两种选择,周期不一样,攻击次数不一样。

数据范围允许我们枚举第二种选择的数量,然后推算出第一种选择的数量,剩余部分一直平A。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;

ll T;
ll n,m;
ll A,B;

inline ll read() {
    ll sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

int main() {
	T = read();
    while(T--) {
        A = read(); B = read(); m = read();
        ll k = B/A;
        if(B%A==0) {
            cout<<(m/A+m/B+2)*160<<"\n";
            continue;
        }
        k++;
        ll res = 0;
        for(ll i=0;i<=m;i+=B) {
            ll wo = k*(i/B);
            ll sheng = m-i;
            ll ans = wo + sheng/(k*A) + sheng/A + 2;
            res = max(res,ans);
        }
        cout<<res*160<<"\n";
    }
    return 0;
}

D:

每个点入度上限为3,简单多了。

首先一个点不能连三个红边,也不能连三个蓝边,所以红边的连通块只能是链或环。

然后发现连成环也允许,一个红色环至少需要三个点,这些点无法通过蓝边连起来。

所以蓝色连通块和红色连通块都是链。

然后发现,链的长度还不能超过四,也就是说最多四个点,因为一个点想要成为红点的链中意味着它要连两个红边,只能连一个蓝边,它就是蓝边的链头或链尾,我们不允许三个链头链尾出现,所以红链只能有两个链中,链长度最多是四。

然后就是分类讨论,一个点,两个点,三个点,四个点。四个情况。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;

ll T;
ll n,m;
ll ans;
vector <ll> e[N],d[N];
map <ll,ll> f,g;
ll si[N];

inline ll read() {
    ll sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

ll check(ll A,ll C) {
    if(!A || !C) return 0;
    return (f[A*n+C] && g[A*n+C]);
}

int main() {
    ll x,y,z;
    n = read(); m = read();
    for(ll i=1;i<=m;i++) {
        x = read(); y = read(); z = read();
        if(z==1) {
            e[x].push_back(y);
            e[y].push_back(x);
            f[x*n+y] = f[y*n+x] = 1;
        }
        else {
            d[x].push_back(y);
            d[y].push_back(x);
            g[x*n+y] = g[y*n+x] = 1;
        }
    }
    for(ll i=1;i<=n;i++) if(e[i].size()==3 || d[i].size()==3) si[i] = 1;
    ans = n;
    ll res = 0;
    for(auto v : f) { if(g[v.first]) res++; }
    ans += res / 2;

    res = 0;
    ll A, B, C, D;
    for(ll i=1;i<=n;i++) {
        if(si[i]) continue;
        A = B = C = D = 0;
        if(e[i].size()==2) A = e[i][0], B = e[i][1];
        if(e[i].size()==1) A = e[i][0];
        if(d[i].size()==2) C = d[i][0], D = d[i][1];
        if(d[i].size()==1) C = d[i][0];
        res += check(A,C) + check(B,C) + check(A,D) + check(B,D);
    }
    ans += res;

    res = 0;
    for(ll i=1;i<=n;i++) {
        if(e[i].size()!=1) continue;
        A = i; B = e[i][0];
        if(e[B].size()!=2) continue;
        if(e[B][0]==A) C = e[B][1];
        else           C = e[B][0];
        if(e[C].size()!=2) continue;
        if(e[C][0]==B) D = e[C][1];
        else           D = e[C][0];
        if(e[D].size()!=1) continue;
        if(g[A*n+B] && g[A*n+D] && g[C*n+D]) res++;
        if(g[A*n+C] && g[A*n+D] && g[B*n+D]) res++;
    }
    ans += res / 2;
    cout<<ans;
    return 0;
}

H:

妙妙树上DP,我写的这个是一个很奇怪的做法。

首先要发现一点:虽然车是按从小到大一个一个寻找空位的,但我们不需要关心它们的具体顺序,只要是一个合法的方案,我们调换任意两辆车的顺序,依旧是合法的。

所以我们dp时就不需要考虑车的编号了,只需要考虑每个点上有几辆车。

最后的答案如何统计,假如第 \(i\) 个点停了 \(b_i\) 辆车,那么这样的方案对应到车辆编号的情况数就有 \(\frac{n!}{b_1!b_2!...b_n!}\) 种,也就是超排列。我们不能让最后的方案数直接乘以 \(n!\) 就是因为有些车是在同一个点的,直接乘以 \(n!\) 会重复计算这一部分。我们 dp 出来的方案数不好统计究竟有多少车在同一个点,但我们发现可以把 \(b_i!\) 分母的这部分先计入我们的dp,也就是说,只要选了一个大小为 \(b_i\) 的点(\(b_i\) 辆车在这个点),我们就让 dp 结果除以 \(b_i\),最后再乘上一个 \(n!\) 就是最终答案了。

dp 方程怎么设,怎么转移,需要观察题目的性质。

因为题目中的过程所有车都是根向走的,我们将时间倒流,每个点上有一辆车,所有车往叶向走回到初始时刻,你会发现一个性质,每一棵子树中车的数量一定不小于子树大小,而且多出来的部分也不会超过这棵子树到根的距离。题目所给的 “随机数据” 的性质就在这里体现了,每个点到根的距离期望值不会超过log。

我们设 \(f[u][i]\) 表示 u 这棵子树中车比点多了 i 个的方案数,儿子们多出来的点数加起来就是父亲多出来的点数,所以这是一个累加求和的背包问题。合并完之后我们要选择 u 这个父亲结点上车的数量,这时我们要让答案除以 \(b_u\),具体含义就是上一段所讲的内容。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>

using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
const ll p=998244353;

ll T;
ll n,m;
vector <ll> e[N];
ll dep[N];
ll f[N][123],g[123];

ll F[N],ni[N];

inline ll read() {
    ll sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

inline ll ksm(ll aa,ll bb) {
    ll sum = 1;
    while(bb) {
        if(bb&1) sum = sum * aa %p;
        bb >>= 1; aa = aa * aa %p;
    }
    return sum;
}

void qiu() {
    F[0] = ni[0] = 1;
    for(ll i=1;i<=N-10;i++) F[i] = F[i-1] * i %p;
    ni[N-10] = ksm(F[N-10],p-2);
    for(ll i=N-11;i>=1;i--) ni[i] = ni[i+1] * (i+1) %p;
}

void DFS(ll u) {
    FOR() {
        ll v = e[u][i];
        dep[v] = dep[u] + 1;
        DFS(v);
    }
}

void TREE(ll u) {
    for(ll v : e[u]) TREE(v);
    f[u][0] = 1;
    ll now = 0;
    for(ll v : e[u]) {
        memset(g,0,sizeof(g));
        for(ll j=0;j<=dep[v];j++) {
            for(ll k=0;k<=now;k++) {
                if(j+k>dep[u]+1) break;
                (g[j+k] += f[u][k] * f[v][j] %p) %= p;
            }
        }
        now += dep[v];
        for(ll j=0;j<=min(now,dep[u]+1);j++) f[u][j] = g[j];
    }
    memset(g,0,sizeof(g));
    for(ll i=0;i<=dep[u]+1;i++) {
        ll duo = i-1;
        for(ll j=0;j<=now;j++) if(duo+j>=0 && duo+j<=dep[u]) (g[duo+j] += f[u][j] * ni[i] %p) %= p;
    }
    for(ll i=0;i<=dep[u];i++) f[u][i] = g[i];
}

int main() {
    int x;
    qiu();
	n = read();
    for(ll i=2;i<=n;i++) {
        x = read();
        e[x].push_back(i);
    }
    DFS(1);
    TREE(1);
    cout<<(f[1][0]*F[n])%p;
    return 0;
}

E:

这题我们捏了个很有趣的东西哈哈,我们称之为 “左偏笛卡尔树”

我们的思路是酱紫的:

首先看字典序最小的拓扑排序,我们找到最大的那个数字,连一条边让它指向右边的整体,表示原图中肯定是先访问了这个点才能访问右边的那些点,否则这个数字就不会出现在这里。

而它左边的哪些数字呢,我们把它们视作并列的兄弟关系,因为即使它们之间相互不连边,在最小拓扑排序中数大的点依旧是后访问。

因此我们可以递归地构建一棵树:找到区间中最大的数,左边的整体成为它的兄弟,右边的整体成为它的儿子。

左右区间各找最大点成为儿子,这样构造的树是标准的笛卡尔树,而我们这个是右边成为儿子,左边成为兄弟,我们形象地称之为 “左偏笛卡尔树”

然后是最大的拓扑排序,原理是一样的,我们继续构造一棵树,把两棵树的所有边加在一起,就是答案的图。(这里要注意第二棵树的边往第一棵树里加时,不允许出现右边的点连向左边这种情况,可以用第一棵树的dfn序来判断,若有这种边直接不合法)

正确性怎么证明呢?

首先是必要性:想要得到题目所给的最小最大拓扑序,我们必须存在这些边。因为我们建图就是为了满足这样的性质,有这些边才能导致这样的顺序。

然后是充分性:只要我们有了这两棵树的所有边,我们就能得到题目所给的最小最大拓扑序。因为在任意一棵树种不存在右侧连向左侧的边,且兄弟结点从左到右依次增大,我们总是会先访问左侧的点,然后是左侧点的儿子(它们比父亲结点优先级更高,因而肯定也要比父亲的右兄弟优先级高),进而访问右侧点,因此一定会得到题目所给的顺序。

证毕。

代码实现:递归造树,RMQ或线段树查询区间最大最小值。

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
#define ls now<<1
#define rs now<<1|1

using namespace std;
const int N=801010;
const int qwq=303030;
const int inf=0x3f3f3f3f;

int T;
int n,m;
int a[N],b[N];
struct E{
    int mx,mi,idx,idi;
}t[N<<2],ling;
vector <int> e[N];
int du[N];
int vis[N];
int st1[N],st2[N],cnt;
int dfn[N],tim;

inline int read() {
    int sum = 0, ff = 1; char c = getchar();
    while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
    while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
    return sum * ff;
}

void add(int u,int v) {
    e[u].push_back(v);
    du[v]++;
}

E pushup(E A,E B) {
    E C = ling;
    if(A.mx > B.mx) C.mx = A.mx, C.idx = A.idx;
    else            C.mx = B.mx, C.idx = B.idx;
    if(A.mi < B.mi) C.mi = A.mi, C.idi = A.idi;
    else            C.mi = B.mi, C.idi = B.idi;
    return C;
}

void built(int now,int l,int r) {
    if(l==r) { t[now] = {a[l],a[l],l,l}; return ; }
    int mid = l+r >> 1;
    built(ls, l, mid);
    built(rs, mid+1, r);
    t[now] = pushup(t[ls],t[rs]);
}

E query(int now,int l,int r,int x,int y) {
    if(x<=l && r<=y) return t[now];
    E res = ling;
    int mid = l+r >> 1;
    if(x<=mid) res = pushup( query(ls, l, mid, x, y), res );
    if(y>mid)  res = pushup( query(rs, mid+1, r, x, y), res );
    return res;
}

void solve(int fa,int l,int r,int cl) {
    if(l>r) return ;
    E wo = query(1, 1, n, l, r);
    if(cl==1) {
        add(fa,wo.mx);
        solve(wo.mx, wo.idx+1, r, cl);
        solve(fa, l, wo.idx-1, cl);
    }
    else {
        if(dfn[wo.mi]<dfn[fa]) { cout<<"No\n"; exit(0); }
        add(fa,wo.mi);
        solve(wo.mi, wo.idi+1, r, cl);
        solve(fa, l, wo.idi-1, cl);
    }
}

void DFS(int u) {
    vis[u] = 1;
    FOR() {
        int v = e[u][i];
        if(vis[v]) continue;
        du[v]--;
        if(!du[v]) DFS(v);
    }
}

void TREE(int u) {
    dfn[u] = ++tim;
    for(int i=e[u].size()-1;i>=0;i--) {
        TREE(e[u][i]);
    }
}

int main() {
    ling = {-inf,inf,0,0};
	n = read();
    for(int i=1;i<=n;i++) {
        a[i] = read();
    }
    built(1, 1, n);
    solve(0, 1, n, 1);
    TREE(0);
    for(int i=1;i<=n;i++) {
        a[i] = read();
    }
    built(1, 1, n);
    solve(0, 1, n, 2);
    for(int i=0;i<=n;i++) {
        if(!du[i] && !vis[i]) DFS(i);
    }
    for(int i=1;i<=n;i++) if(!vis[i]) {cout<<"No\n"; return 0;}
    for(int i=1;i<=n;i++) {
        for(int v : e[i]) {
            st1[++cnt] = i; st2[cnt] = v;
        }
    }
    cout<<"Yes\n";
    cout<<cnt<<"\n";
    for(int i=1;i<=cnt;i++) cout<<st1[i]<<" "<<st2[i]<<endl;
    return 0;
}
posted @ 2024-04-19 01:45  maple276  阅读(206)  评论(0编辑  收藏  举报