跳转至

模拟赛记录

2024.7.24 模拟赛记录

T1 - 采果子

采果子

Sol

注意到图是完全图,且我们只能从果子数低向果子数高走,因此先考虑将其按照 \(a[i]\) 从小到大排序,这样之后再进行操作可以确保无后效性

考虑 \(dp\);题目问什么就设什么,尝试设 \(dp[i][j]\) 表示走到第 \(i\) 棵树 还剩 \(j\) 小时,同时 摘走第 \(i\) 棵树的果子 能获得的最大价值;其实转移比较类似 \(01\) 背包,考虑枚举当前第 \(i\) 棵树与容量 \(j\),再枚举前面的某一棵树 \(k\),转移即为从 \(k\) 走到 \(i\);因此,转移方程为 \(dp[i][j]=dp[k][j-dis[k][i]-c[i]]+s[i]\),其中 \(c[i]\) 表示摘第 \(i\) 棵树上果子的时间,\(s[i]\) 表示摘第 \(i\) 棵树上果子的价值

注意,因为我们可以从任何一棵树开始,比较方便的一种处理方法是建立一个超级原点 \(0\),自身的所有信息都是 \(0\),再向所有树都连一条权值为 \(0\) 的边

对于初值,直接赋 \(0\) 即可,因为浪费了一些时间的初始情况一定比不浪费是更劣的,不会对答案造成影响;对于答案,我们枚举每个状态,取最大值即可

时间复杂度 \(O(n^2m)\)

C++
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN = 105, MAXM = 1005, INF = 1e9;

int n, m;
ll ans;
ll d[MAXN][MAXN];
ll dp[MAXN][MAXM];

struct Tree{
    ll a, s, c, id;
}tree[MAXN];

bool cmp(Tree t1, Tree t2){
    return t1.a < t2.a;
}

int main(){
    scanf("%d%d", &n, &m);

    for (int i=1; i<=n; i++){
        scanf("%lld%lld%lld", &tree[i].a, &tree[i].s, &tree[i].c);
        tree[i].id = i;
    }
    for (int i=1; i<=n; i++){
        d[0][i] = 0;
        for (int j=1; j<=n; j++){
            scanf("%lld", &d[i][j]);
        }       
    }
    tree[0].a = tree[0].s = tree[0].c = tree[0].id = 0;
    sort(tree, tree+n+1, cmp);

    for (int i=0; i<=n; i++){
        for (ll j=m; j>=0; j--){
            for (int k=0; k<i; k++){
                if (j >= d[tree[k].id][tree[i].id]+tree[i].c && tree[k].a<tree[i].a){
                    dp[i][j] = max(dp[i][j], dp[k][j-d[tree[k].id][tree[i].id]-tree[i].c]+tree[i].s);
                }   
            }
        }
    }
    for (int i=1; i<=n; i++){
        for (int j=0; j<=m; j++){
            // cout << i << " " << j << " " << dp[i][j] <<endl;
            ans = max(dp[i][j], ans);
        }
    }

    printf("%lld", ans);
    return 0;
}

Record

别被图论限制了思路啊,这个 \(dp\) 还是比较明显的;现在 \(dp\) 的水平真的太差了,得多练。

T2 - 非常计划

非常计划

Sol

玩文字游戏是比较难评的……

最短路计数板子,注意 有重边就取最小值,这是由于所有边完全重合只算一次,因此有多条重边对结果没有影响;因为要取最小值,用邻接矩阵会比较方便,\(O(n^2)\) 是可以接受的

时间复杂度 \(O(n^2)\)

C++
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN = 2005, INF = 1e9;

int N, M;

struct Edge{
    int to;
    int weig;
};

int G[MAXN][MAXN];
int dis[MAXN], vis[MAXN];
ll cnt[MAXN];

void dijkstra(int start){
    memset(dis, 0x7f, sizeof(dis));
    dis[start] = 0;
    cnt[start] = 1;

    for (int i=1; i<=N; i++){
        int recent = 0, recentmin = INF;
        for (int j=1; j<=N; j++){
            if (vis[j] == false && dis[j] < recentmin){
                recent = j;
                recentmin = dis[j];
            }   
        }
        vis[recent] = true;
        for (int j=1; j<=N; j++){
            if (G[recent][j] != INF){
                if (dis[j] >= dis[recent]+G[recent][j]){
                    if (dis[j] == dis[recent]+G[recent][j]){
                        cnt[j] += cnt[recent];
                    }
                    else{
                        dis[j] = dis[recent]+G[recent][j];
                        cnt[j] = cnt[recent];
                    }
                }
            }
        }   
    }
}

int main(){
    scanf("%d%d", &N, &M);

    for (int i=1; i<=N; i++){
        for (int j=1; j<=N; j++){
            G[i][j] = INF;
        }
    }

    for (int i=1; i<=M; i++){
        int node1, node2, weig;
        scanf("%d%d%d", &node1, &node2, &weig);
        G[node1][node2] = min(G[node1][node2], weig);
    }

    dijkstra(1);

    if (dis[N] > INF){
        printf("No answer");
        return 0;
    }
    printf("%d %lld", dis[N], cnt[N]);
    return 0;
}

Record

\(what\ can\ I\ say?\) 抽象艺术出题人

T3 - 方程的解

P1771

Sol

首先,\(g(x)\) 可以直接上快速幂解决,即为求不定方程 \(\sum_{i=1}^k a_i= p\) 解的数量,\(p\) 是定值

题目要求 \(a_i (1 \le i \le k)\) 为正整数,即将 \(p\) 划分为 \(k\) 个正整数相加,这就是经典插板法;在 \(p-1\) 个空隙中插 \(k-1\) 个板,答案即为 \(C_{p-1}^{k-1}\),别忘了上高精度,\(k\) 很小,一个一个乘完再一个一个除即可

代码

C++
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MOD = 1000, MAXL = 5e3+5;

int k, x, bas;
int ans[MAXL];

int QuickPow(int a, int b){
    ll res = 1;
    while (b){
        if (b&1) res = (res*a)%MOD;
        a = (a*a)%MOD;
        b >>= 1;
    }
    return res;
}

void mul(int x){
    int i;
    for (i=1; i<=5e3; i++){
        ans[i] = ans[i]*x;
    }
    for (i=1; i<=5e3; i++){
        ans[i+1] += ans[i]/10;
        ans[i] = ans[i]%10;
    }
}

void div(int x){
    int i, len, m;
    for (len=5e3; ans[len]==0&&len>=1; len--);  
    for (i=len, m=0; i>=1; i--){
        int tmp = ans[i];
        ans[i] = (m*10+tmp)/x;
        m = (m*10+tmp)%x;
    }
}

void prt(){
    int i;
    for (i=5e3; ans[i]==0&&i>1; i--);
    for (; i>=1; i--){
        printf("%d", ans[i]);
    }
}

int main(){
    scanf("%d%d", &k, &x);
    bas = QuickPow(x%MOD, x)-1;

    ans[1] = 1;
    for (int i=0; i<k-1; i++){
        int recent = bas-i; 
        mul(recent);
    }
    // prt();
    // cout << endl;
    for (int j=2; j<=k-1; j++){
        div(j);
    }
    prt();
    return 0;
}

Record

高精度不能忘啊,高精除单精还是得会的(

T4 - 染色

P3177

Sol

答案即求 \(\sum_{color} \sum_{1 \le i < j \le n} \sum_{e \in \delta(i, j)} w(e)\),其中 \(i\)\(j\) 都染了 \(color\) 这一颜色,\(\delta(i, j)\) 表示 \(i\)\(j\) 间的简单路径;我们尝试求和换序,即求 \(\sum_{e \in tree} w(e) \sum_{color} \sum_{1 \le i < j \le n}\),其中 \(\delta(i, j)\) 包含 \((s, t)\);因此转化为统计 树上有多少同色点经过一条边

考虑记录 \(dp[i][j]\) 表示节点 \(i\) 的子树内有 \(j\) 个点染成黑色的最大贡献;对于当前节点 \(i\) 与其的一个孩子 \(child\),设在 \(child\) 中选 \(k\) 个点染黑,则边 \((i, child)\) 会产生两部分贡献;黑色点的贡献为 \(k \times (m-k) \times w(e)\),白色点的贡献为 \((siz[child]-k) \times (n-siz[child]-(m-k)) \times w(e)\) 其中 \(siz\) 表示子树大小;两部分加起来即为该边产生的贡献,记为 \(val\)

设在当前节点 \(i\)\(j\) 个染黑,在儿子 \(child\) 中选 \(k\) 个点染黑,则转移方程为 \(dp[i][j] = max(dp[i][j-k]+dp[child][k]+val)\);注意需要倒序枚举 \(j\),否则会产生重复贡献,与 \(01\) 背包滚动数组的原理类似

注意 \(int \times int\) 时爆 \(long long\)

代码

C++
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN = 2005, MAXM = 2005;

int n, m;

struct Edge{
    int to;
    int weig;
};

vector <Edge> G[MAXN];
ll dp[MAXN][MAXM];
int siz[MAXN];

void dfssiz(int node, int fa){
    siz[node] = 1;
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i].to == fa) continue;
        dfssiz(G[node][i].to, node);
        siz[node] += siz[G[node][i].to];        
    }
}

void dfs(int node, int fa){
    dp[node][0] = dp[node][1] = 0;
    for (int i=0; i<G[node].size(); i++){
        if (G[node][i].to == fa) continue;
        dfs(G[node][i].to, node);
        for (int j=min(siz[node], m); j>=0; j--){
            for (int k=0; k<=min(siz[G[node][i].to], j); k++){
                if (dp[node][j-k] == -1) continue;
                ll cnt = ((siz[G[node][i].to]-k)*(n-siz[G[node][i].to]-(m-k)))*(ll)G[node][i].weig + k*(m-k)*(ll)G[node][i].weig;
                dp[node][j] = max(dp[node][j], dp[node][j-k]+dp[G[node][i].to][k]+cnt);
            }
        }
    }
}

int main(){
    scanf("%d%d", &n, &m);

    for (int i=1; i<=n-1; i++){
        int node1, node2, weig;
        scanf("%d%d%d", &node1, &node2, &weig);
        G[node1].push_back({node2, weig});
        G[node2].push_back({node1, weig});
    }

    memset(dp, -1, sizeof(dp));

    dfssiz(1, 1);
    dfs(1, 1);

    printf("%lld", dp[1][m]);
    return 0;
}

Record

\(dp\) 向来是会不了一点的;考场上注意读题!写链的部分分的时候糊的 \(dp\) 只考虑了一种颜色的点,什么实力(

2024.7.21 模拟赛(CSPS2022)记录

T1 - P8817

P8817

Sol

首先,我们显然要预处理出每两个点需要换乘几次车,这个容易通过在每一个点跑一次 \(bfs\) 得出,时间复杂度 \(O(n^2)\),可以接受;

我们考虑贪心,从节点 \(1\) 走到离可以走到的最大价值点,再从这个最大价值点走到下一个最大价值点……但这样对吗?好像有点问题;我们走到当前最大价值点时,实际上对下一步的范围有影响,进而影响下一步能走到的最大价值点,因此有后效性,不能直接贪;但是我们如果枚举每个点,\(O(n^4)\) 的时间复杂度显然无法接受,所以我直接暴力跑路

考虑优化我们的贪心;什么时候不会产生后效性?这个简单, 后面没有点 了呗,因此设路径为 \(1-a-b-c-d-1\),当我们确定 \(a,b,c\) 时,就可以贪 \(d\);又注意到整个路径是对称的,因此我们 把整个路径分为两组,即 \(\{a, b\}\)\(\{c, d\}\);在确定 \(b\) 时可以贪 \(a\),在确定 \(c\) 时可以贪 \(d\)

因此,我们还要处理出确定一个点 \(node\) 时,可以到达点 \(node\) 且可以到达点 \(1\) 的最大价值点;这其实可以在开始的 \(bfs\) 中一并处理,具体的,当我们确定点 \(node\) 可达 \(i\) 时,再判断点 \(1\) 是否可达 \(i\);我们记录这样可达的点 \(i\),直接排序即可

问题是,我们如果记录下所有的点 \(i\),那时间复杂度又炸了;怎么办?其实不需要用这么多点的,设已知点 \(b\),点 \(i\) 只可能与点 \(c\)、点 \(d\) 重合,因此最多跳 \(2\) 次,所以我们只需要记录下 \(3\) 个这样的点 \(i\) 即可

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2505, MAXL = 5e5+5;

int n, m, k, front, rear;
long long ans;
long long w[MAXN];
vector <int> G[MAXN];

struct Node{
    int node;
    int step;
}q[MAXL];

bool vis[MAXN];
bool cget[MAXN][MAXN];
vector <int> mget[MAXN];

bool cmp(int a, int b){
    return w[a]>w[b];
}

void bfs(int start){
    memset(vis, false, sizeof(vis));
    vis[start] = true;
    front = rear = 0;
    q[rear].node = start;
    q[rear].step = 0;
    rear++;

    while (front != rear){
        int rnode = q[front].node;
        int rstep = q[front].step;
        front++;

        if (rnode != start){
            cget[start][rnode] = true;
            if (cget[1][rnode] == true){
                mget[start].push_back(rnode);
                sort(mget[start].begin(), mget[start].end(), cmp);
                if (mget[start].size() >= 4) mget[start].pop_back();
            }
        }
        if (rstep > k) continue;
        for (int i=0; i<G[rnode].size(); i++){
            if (vis[G[rnode][i]] == false){
                vis[G[rnode][i]] = true;
                q[rear].node = G[rnode][i];
                q[rear].step = rstep+1;
                rear++;
            }
        }
    }
}

int main(){
    scanf("%d%d%d", &n, &m, &k);
    for (int i=2; i<=n; i++){
        scanf("%lld", &w[i]);
    }
    for (int i=1; i<=m; i++){
        int node1, node2;
        scanf("%d%d", &node1, &node2);
        G[node1].push_back(node2);
        G[node2].push_back(node1);
    }

    for (int i=1; i<=n; i++){
        bfs(i);
    }

    for (int i=2; i<=n; i++){
        for (int j=2; j<=n; j++){
            if (i == j || cget[i][j] == false) continue;
            for (int p=0; p<mget[i].size(); p++){
                for (int q=0; q<mget[j].size(); q++){
                    if (i == mget[i][p] || i == mget[j][q] || j == mget[i][p] || j == mget[j][q] || mget[i][p] == mget[j][q]){
                        continue;
                    }
                    ans = max(ans, w[i]+w[j]+w[mget[i][p]]+w[mget[j][q]]);
                }
            }
        }
    }

    printf("%lld", ans);
    return 0;
}

Record

赛时可真是抽象...打完 \(40pts\)\(O(n^4)\) 暴力后就直接开后面的题,浪费了太多时间;虽然最后也没有想出正解,但也没有时间交上优化后的暴力,比赛前 \(1min\) 卡线没交上,\(60pts \rightarrow 45pts\);所以还是记得别太极限……

T2 - P8818

P8818

简单题。我们考虑对 \(a[i] (l_1 \le i \le r_1)\) 全正/全负/有正有负 进行分讨,对于 \(b[i] (l_2 \le i \le r_2)\) 同理,过程应该可以优化,但这么讨论最直接,适合我这种没脑子的人

分讨中,我们发现需要记录 \(a[i]\) 任意区间的最大值 / 最小值 / 非负数最小值 / 非正数最大值、\(b[i]\) 任意区间的最大值 / 最小值,直接枚举显然会炸,上相关数据结构即可,线段树或者 \(ST\) 表都行;

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+5, MAXLOG = 20, INF = 1e9;

int n, m, q, l1, r1, l2, r2, len1, len2;
long long m1, m2, m3, m4, m5, m6, m7, m8;
long long a[MAXN], b[MAXN], log_2[MAXN];
long long c1[MAXN][MAXLOG], c2[MAXN][MAXLOG], c3[MAXN][MAXLOG], c4[MAXN][MAXLOG];
long long d1[MAXN][MAXLOG], d2[MAXN][MAXLOG];

void init(){
    log_2[1] = 0;
    log_2[2] = 1;

    for (int i=3; i<=max(n, m); i++){
        log_2[i] = log_2[i/2]+1;
    }
}

int main(){
    scanf("%d%d%d", &n, &m, &q);

    for (int i=1; i<=n; i++){
        scanf("%lld", &a[i]);
        c1[i][0] = a[i];
        c2[i][0] = a[i];
        if (a[i]==0) c3[i][0] = a[i], c4[i][0] = a[i];
        else if (a[i]>0) c3[i][0] = a[i], c4[i][0] = -INF;
        else if (a[i]<0) c3[i][0] = INF, c4[i][0] = a[i];
    }
    for (int i=1; i<=m; i++){
        scanf("%lld", &b[i]);
        d1[i][0] = b[i];
        d2[i][0] = b[i];
    }

    init();

    for (int j=1; j<=MAXLOG; j++){
        for (int i=1; (i+(1<<j)-1)<=n; i++){
            c1[i][j] = max(c1[i][j-1], c1[i+(1<<(j-1))][j-1]);
            c2[i][j] = min(c2[i][j-1], c2[i+(1<<(j-1))][j-1]);
            c3[i][j] = min(c3[i][j-1], c3[i+(1<<(j-1))][j-1]);
            c4[i][j] = max(c4[i][j-1], c4[i+(1<<(j-1))][j-1]);

        }   
    }   
    for (int j=1; j<=MAXLOG; j++){
        for (int i=1; (i+(1<<j)-1)<=m; i++){
            d1[i][j] = max(d1[i][j-1], d1[i+(1<<(j-1))][j-1]);
            d2[i][j] = min(d2[i][j-1], d2[i+(1<<(j-1))][j-1]);
        }   
    }   

    for (int i=1; i<=q; i++){
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        len1 = r1-l1+1;
        len2 = r2-l2+1;
        m1 = max(c1[l1][log_2[len1]], c1[r1-(1<<log_2[len1])+1][log_2[len1]]);
        m2 = min(c2[l1][log_2[len1]], c2[r1-(1<<log_2[len1])+1][log_2[len1]]);
        m3 = max(d1[l2][log_2[len2]], d1[r2-(1<<log_2[len2])+1][log_2[len2]]);
        m4 = min(d2[l2][log_2[len2]], d2[r2-(1<<log_2[len2])+1][log_2[len2]]);
        m5 = min(c3[l1][log_2[len1]], c3[r1-(1<<log_2[len1])+1][log_2[len1]]);
        m6 = max(c4[l1][log_2[len1]], c4[r1-(1<<log_2[len1])+1][log_2[len1]]);
        // cout << m1<< " " << m2 << " " << m3 << " " << m4 << " " << m5 << " " << m6 << endl;
        if (m3 <= 0){
            if (m1 <= 0) printf("%lld\n", m2*m3);
            else if (m2 >= 0) printf("%lld\n", m2*m4);
            else printf("%lld\n", m2*m3);
        }
        else if (m4 >= 0){
            if (m1 <= 0) printf("%lld\n", m1*m3);
            else if (m2 >= 0) printf("%lld\n", m1*m4);
            else printf("%lld\n", m1*m4);
        }
        else{
            if (m1 <= 0) printf("%lld\n", m1*m3);
            else if (m2 >= 0) printf("%lld\n", m2*m4);
            else printf("%lld\n", max(m3*m6, m4*m5));       
        }
    }
    return 0;
}

Record

这题本身没什么难度,主要想说说拍子的事……写了个 \(py\) 文件当生成器,结果运行时写成 \(python3 \ xxx.py\),电脑恰好没 \(python3\),再加上我眼瞎把提示看成 \(fc\) 的提示 (什么实力),愣是一直没发现……\(1h\) 后才发现并改成了 \(python \ xxx.py\)

T3 - P8819

P8819

一个节点 \(i\) 能实现反击,意味着从 \(i\) 可以走到一个环;能实现连续穿梭,意味着图中所有节点的出度都是 \(1\);事实上,所有节点的出度都是 \(1\) 已经包含从任意节点 \(i\) 都能走到环这一条件,因此我们 只需要记录出度

对于操作 \(1\) 与操作 \(3\),很容易维护;但是对于操作 \(2\) 与操作 \(4\) 不太好办,因为我们删除或者恢复一个点,操作的是这个节点的入边;如果我们枚举每个与 \(i\) 连边的点,时间开销太大,无法接受;由于操作 \(1\) 与操作 \(3\) 对于入度也很容易维护,因此我们考虑 改为维护入度

出度与入度有什么关系?有一个显然的基本定理:所有点的入度之和等于所有点的出度之和;因此,如果我们已知所有点的入度之和,可以保证结果的 正确性,但是,此时充分性无法保证;例如,我们知道 \(1+1+1+1=4\),但是只知道和是 \(4\),无法确定每个点的出度都是 \(1\);这里是关键点:我们 考虑哈希与随机化的思想

原来所有点的出度都是 \(1\),算出和来不好反推;我们将 每个点随机赋一个值,代表其入度;记录其一开始的入度之和,如果某次操作后当前所有点的入度之和等于开始的入度之和,那么我们就可以认为此时满足要求

实现时,开一个变量实时维护所有点的入度之和,我们承受不了每次操作后再加在一块统计的时间开销……

代码

C++
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e5+5;

int n, m, q, node1, node2, op;
unsigned int s=0, rs=0;
unsigned int w[MAXN], in[MAXN], r[MAXN];

int main(){
    srand(time(0));
    scanf("%d%d", &n, &m);
    for (int i=1; i<=n; i++){
        w[i] = (unsigned int)rand();
        s += w[i];
    }
    for (int i=1; i<=m; i++){
        scanf("%d%d", &node1, &node2);
        r[node2] += w[node1];
        in[node2] += w[node1];
        rs += w[node1];
    }
    scanf("%d", &q);
    for (int i=1; i<=q; i++){
        scanf("%d", &op);
        if (op == 1){
            scanf("%d%d", &node1, &node2);
            in[node2] -= w[node1];
            rs -= w[node1];
        }
        else if (op == 2){
            scanf("%d", &node1);
            rs -= in[node1];
            in[node1] = 0;
        }
        else if (op == 3){
            scanf("%d%d", &node1, &node2);
            in[node2] += w[node1];
            rs += w[node1];
        }
        else if (op == 4){
            scanf("%d", &node1);
            rs += r[node1]-in[node1];
            in[node1] = r[node1];
        }
        if (rs == s){
            printf("YES\n");
        }
        else{
            printf("NO\n");
        }
    }
    return 0;
}

Record

太菜了,考试的时候直接照着最暴力的部分打,甚至没有考虑要求 \(1\) 实际上没有意义;\(40pts \rightarrow 25pts\);不过这题的正解思想确实精妙,现在根本没有这个实力……

T4 - P8820

P8820

不会捏。\(44pts\) 暴力跑路;其实如果纯写暴力,对于我这种 \(dp\) 极度不好的人来说没有必要去写 \(dp\),直接暴力建图 + 堆优化 \(dij\) 即可,时间复杂度可能还要更优……