2024暑假集训补题专辑(重发)

之前写的博客每篇只有一两题,太散了,而且现在来看有些题没必要留着。整合重发一下。


2024CCPC长春邀请赛(吉林省赛)

B. Dfs Order 0.5

题意

给出一棵树,节点编号从1到n,1号节点为根。每个节点都有一定的权重。定义一个dfs序的得分为:在dfs序中的编号(以下简称为序号)为偶数的的节点的权重之和。其中1号节点序号为1。求最大得分。

解法

此题显然是树形DP,维护以每个节点为根的子树可以获得的最大得分。如果以x号节点为根的子树共有奇数个节点,不妨称x为奇数点,反之为偶数点。在x的序号分别为奇数、偶数的情况下,设以x为根的子树的最大得分分别为val[x][1]和val[x][0]。如果x的子节点全为偶数点,那么它们序号的奇偶性一定与x不同;如果x的子节点中存在奇数点,那么所有偶数子节点的序号的奇偶性可以任取[1],而对于所有奇数子节点,序号为奇数和偶数的各占一半[2]

[1] 考虑第一个走到的奇数子节点s,如果一个偶数子节点在s之前走到,那它序号的奇偶性就和s相同,也就是和根x不同;如果在s之后走到,则序号奇偶性和x相同。所以可以任取。
[2] 每走完一棵以奇数点为根的子树,序号奇偶性都会切换奇数次,也就是说,下一个走到的子节点的序号的奇偶性和当前这个一定不同。所以奇偶各占一半。

可以先假设所有奇数子节点的序号都是奇数,现在需要把其中一部分改为偶数。对于每个奇数子节点s,算出序号由奇变为偶的得分增益

tr[s] = val[s][0] - val[s][1]

把tr数组从大到小排序,取前一半就好了。
然后就做完了。最终答案就是val[1][1]。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
#define ll long long
using namespace std;
const int N = 2e5 + 5;

int ti, n;
bool vis[N], f[N];
ll a[N], val[N][2], tr[N];
vector<int> ve[N];

bool cmp(ll t1, ll t2){
    return t1 > t2;
}

void dfs(int x){
    vis[x] = 1;
    int sz = ve[x].size();
    if(x != 1 && sz == 1){
        val[x][0] = a[x];
        return;
    }
    int pos = -1;
    bool flagji = 0, flagou = 0;
    for(int i = 0; i < sz; i++){
        int to = ve[x][i];
        if(!vis[to]){
            dfs(to);
            if(f[to]) flagou = 1;
            else flagji = 1;
        }
        else pos = i;
    }
    if(!flagji){
        for(int i = 0; i < sz; i++){
            if(i == pos) continue;
            int to = ve[x][i];
            val[x][0] += val[to][1];
            val[x][1] += val[to][0];
        }
        val[x][0] += a[x];
        return;
    }
    int p = 0;
    for(int i = 0; i < sz; i++){
        if(i == pos) continue;
        int to = ve[x][i];
        if(f[to]){
            val[x][0] += max(val[to][0], val[to][1]);
            continue;
        }
        tr[++p] = val[to][0] - val[to][1];
        val[x][0] += val[to][1];
    }
    if(p % 2) f[x] = 1;
    sort(tr + 1, tr + 1 + p, cmp);
    for(int i = 1; i <= p / 2; i++){
        val[x][0] += tr[i];
    }
    val[x][1] = val[x][0];
    if(p % 2) val[x][1] += tr[p / 2 + 1];
    val[x][0] += a[x];
}

int main(){
    scanf("%d", &ti);
    while(ti--){
        scanf("%d", &n);
        for(int i = 1; i <= n; i++){
            scanf("%lld", &a[i]);
            val[i][0] = 0;
            val[i][1] = 0;
            f[i] = 0;
            vis[i] = 0;
            ve[i].clear();
        }
        int u, v;
        for(int i = 1; i < n; i++){
            scanf("%d%d", &u, &v);
            ve[u].push_back(v);
            ve[v].push_back(u);
        }
        if(n == 1){
            printf("0\n");
            continue;
        }
        dfs(1);
        printf("%lld\n", val[1][1]);
    }
    return 0;
}

回到顶部


XXI Open Cup GP of Tokyo

B. Bit Operation

题意

给定一个01序列a,可以执行n-1次如下操作:选择相邻的两个数a[i]和a[i+1],将它们替换为a[i] & a[i+1]或a[i] | a[i+1](这是两种不同的操作)。问有多少种方案,可以使最后剩下的那一个数是1。

解法

考虑题中给出的操作,选定的两个数只有四种可能的情况:

 0 & 0 = 0, 0 | 0 = 0
 1 & 1 = 1, 1 | 1 = 1
 0 & 1 = 0, 0 | 1 = 1
 1 & 0 = 0, 1 | 0 = 1

可以发现,如果两个数有0有1,&可以看作删掉其中的1,|可以看作删掉其中的0;如果两个数相同,&可以看作取左边的数,|可以看作取右边的数(当然,左右反过来也一样)。那么这个操作就可以等价于:

选择两个相邻的数,删掉其中一个;删左和删右是两种不同的操作。

这样就可以枚举a中每一个1,作为最后留下来的数。接下来考虑把选定的位置左右两端的所有数全部删掉的方案数。先考虑删除a[i](i不是选定的位置)的方案数。如果i=1,那么只能取a[1]和a[2],删除a[1],也就是只有一种方案。i=n与此同理。否则,可以取a[i-1]和a[i],删除a[i],或者取a[i]和a[i+1],删除a[i],也就是有两种方案。即:删掉第一个数的方案只有一种,而删掉其他数的方案都有2种
用p[i]表示选定位置的某一侧有i个数时,把它们全部删掉的方案数。显然,p[0] = 1。由上述结论,这一侧有i个数时,第一次操作有(1+2i)种选择。做完这次操作,还剩i-1个数,全部删掉的方案数就是p[i-1]。所以可以通过

p[0] = 1;
for(int i = 1; i <= n; i++){
    p[i] = p[i - 1] * (i * 2 - 1);
}

得出删掉一侧任意个数的方案数(实际做题注意取模)。

由于左右两端的操作可以相互穿插,假设选定的位置是i,那也就是说总共做了n-1次操作,其中有i-1次是对左边进行的,所以答案要再乘上组合数C(n-1, i-1)。i位置的最终答案就是p[i-1] * p[n-i] * C(n-1, i-1)。最后把所有选定位置的答案加起来即可。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
const int N = 1e6 + 5;
const ll P = 998244353;

int n, a[N];
ll p[N], inv[N], c[N], ans;

ll fastpow(ll x, ll y){
    ll ret = 1;
    while(y){
        if(y & 1) ret = ret * x % P;
        x = x * x % P;
        y >>= 1;
    }
    return ret;
}

int main(){
    scanf("%d", &n);
    for(int i = 1; i <= n; i++){
        scanf("%d", &a[i]);
    }

    //p[i]表示选定的数的某一侧有i个数时,删掉这些数的方案数
    p[0] = 1;
    for(int i = 1; i <= n; i++){
        p[i] = p[i - 1] * (i * 2 - 1) % P;
    }

    //inv[i]是i的逆元
    for(int i = 1; i <= n; i++){
        inv[i] = fastpow((ll)i, P - 2);
    }

    //c[i]表示组合数C(n - 1, i)
    c[0] = 1;
    for(int i = 1; i <= n; i++){
        c[i] = (c[i - 1] * (n - i) % P) * inv[i] % P;
    }

    //对每个a[i] = 1的位置求出答案
    for(int i = 1; i <= n; i++){
        if(a[i]){
            ans = (ans + (p[i - 1] * p[n - i] % P) * c[i - 1] % P) % P;
        }
    }
    printf("%lld\n", ans);
    return 0;
}

回到顶部


2022CCPC威海站

D. Sternhalma

题意

跳棋。给一个如题中图所示的棋盘,每个格子都带有一个分值,棋盘上某些格子里有一个棋子。可以执行两种操作:1.随便移去一个棋子,不得分;2.设有相邻且共线三个格子u,v,w(v在中间),如果u,v中都有棋子而w没有,可以把u中棋子越过v跳到w位置,然后移去v中棋子,获得v处的分值。现在有n次询问,每次询问告诉你每个格子有无棋子,问能得到的最大分值。

解法

由于只有19个格子,考虑状压,可以用一个数来表示棋盘上的一个棋子摆放状态。手动打出可行的每种状态转移方式,然后类似暴力模拟,求出当前状态的最大分值即可。如果写法不够好而导致TLE,可以在所有询问开始之前,从0开始反向转移状态,求出每个初始状态的最大分值,每次询问后直接输出对应结果即可。

听说有傻子看完题之后整个思路都对,就没想到用状压,真离谱吧,不知道是谁。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
const int N = 25;
const int M = 1e6 + 5;

int scr[N], n, cnt[M];
char ch[N];

queue<int> q;
int tp;
bool inq[M];

struct node{
    int u, v, w;
    node(int u1 = 0, int v1 = 0, int w1 = 0){
        u = u1;
        v = v1;
        w = w1;
    }
};
vector<node> ve;

void d(int uu, int vv, int ww){
    ve.push_back(node(uu, vv, ww));
    ve.push_back(node(ww, vv, uu));
}
void init(){
    d(0,1,2); d(2,6,11); d(11,15,18); d(16,17,18); d(7,12,16); d(0,3,7);
    d(0,4,9); d(1,4,8); d(3,4,5); d(1,5,10); d(2,5,9); d(4,5,6);
    d(3,8,13); d(4,8,12); d(7,8,9); d(4,9,14); d(5,9,13); d(8,9,10); d(5,10,15); d(6,10,14); d(9,10,11);
    d(8,13,17); d(9,13,16); d(12,13,14); d(9,14,18); d(10,14,17); d(13,14,15);
}

int main(){
    init();
    int sz = ve.size();
    cin.tie(0);
    for(int i = 0; i < 19; i++){
        cin >> scr[i];
    }

    cnt[0] = 0;
    inq[0] = 1;
    q.push(0);
    int tp, to;
    while(!q.empty()){
        tp = q.front();
        q.pop();
        for(int i = 0; i < 19; i++){
            if(!(tp & (1 << i))){
                to = tp | (1 << i);
                if(!inq[to]){
                    inq[to] = 1;
                    q.push(to);
                }
                cnt[to] = max(cnt[to], cnt[tp]);
            }
        }
        for(int i = 0; i < sz; i++){
            if((!(tp & (1 << ve[i].u))) && (!(tp & (1 << ve[i].v))) && (tp & (1 << ve[i].w))){
                to = tp - (1 << ve[i].w) + (1 << ve[i].u) + (1 << ve[i].v);
                if(!inq[to]){
                    inq[to] = 1;
                    q.push(to);
                }
                cnt[to] = max(cnt[to], cnt[tp] + scr[ve[i].v]);
            }
        }
    }

    cin >> n;
    while(n--){
        int st = 0;
        for(int i = 0; i < 19; i++){
            cin >> ch[i];
            if(ch[i] == '#') st += (1 << i);
        }
        cout << cnt[st] << endl;
    }
    return 0;
}

回到顶部


I. Dragon Bloodline

题意

用一些元素合成龙蛋。合成每个龙蛋需要n种元素,第i种元素需要a[i]个;有k种工具龙可以用来收集元素,第i种工具龙有b[i]个,它们中每个可以任选一种元素,收集2i-1个该种元素。问最多能合成多少个龙蛋。

解法

读完题就在纠结,如何判断一个工具龙是收集需求量大的元素好,还是收集很多需求量少的元素好?很难判断。但是如果先假定一个要合成的龙蛋的个数,再判断能否合成这么多龙蛋,就比较简单:
建立一个优先队列,里面存储每一种元素的总需求量。尽量用收集量大的工具龙,去收集需求量大的元素。这样,浪费掉的收集量一定是最小的。因为收集一定量的元素之后,考虑剩下的工具龙,它们的收集量越“零散”,在后续过程中越不容易造成浪费。
所以,上述过程套上二分答案就行了。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define ll long long
using namespace std;
const int N = 5e4 + 5;

int ti, n, k;
ll a[N], b[25], c[25], num[25];
priority_queue<ll> q;

bool check(ll x){
    while(!q.empty()) q.pop();
    for(int i = 1; i <= n; i++){
        q.push(a[i] * x);
    }
    ll tp, db;
    int p = k;
    while(!q.empty()){
        if(!p) return 0;
        tp = q.top();
        q.pop();
        if(tp > num[p]){
            db = min(tp / num[p], b[p]);
            b[p] -= db;
            tp -= db * num[p];
            if(!b[p]) p--;
            if(tp) q.push(tp);
        }
        else{
            b[p]--;
            if(!b[p]) p--;
        }
    }
    return 1;
}

int main(){
    scanf("%d", &ti);
    while(ti--){
        scanf("%d%d", &n, &k);
        ll totnd = 0;
        for(int i = 1; i <= n; i++){
            scanf("%d", &a[i]);
            totnd += a[i];
        }
        for(int i = 1; i <= k; i++){
            scanf("%d", &b[i]);
            c[i] = b[i];
        }
        num[1] = 1;
        for(int i = 2; i <= k; i++){
            num[i] = num[i - 1] * 2;
        }
        ll totnum = 0;
        for(int i = 1; i <= k; i++){
            totnum += num[i] * b[i];
        }
        ll l = 1, r = totnum / totnd, mid, ans = 0;
        bool rst;
        while(l <= r){
            mid =(l + r) / 2;
            for(int i = 1; i <= k; i++){
                b[i] = c[i];
            }
            rst = check(mid);
            if(!rst){
                r = mid - 1;
            }
            else{
                ans = max(ans, mid);
                l = mid + 1;
            }
        }
        printf("%lld\n", ans);
    }
    return 0;
}

回到顶部


NWERC 2019

G. Gnoll Hypothesis

题意

不细说了,大家应该都做了而且都会做,但好像都用组合数算的,我来点不太一样的思路。

解法

n个怪物有k个要被选中,现在考虑x号怪物,在所有方案中它被选中的概率其实就是k/n,它的答案就加上s[x] * k/n。接下来还剩n-1个怪物,要选k-1个,也就是要有(n-1)-(k-1) = (n-k)个不被选中,那x的前一个怪物不被选中的概率其实就是(n-k)/(n-1),答案就加上s[x-1] * k/n * (n-k)/(n-1)。依此类推。也就是说初始概率p为n/k,往前推到x-i个位置,p就乘上(n-k-i+1)/(n-i),然后x位置的答案加上s[x-i] * p即可。
虽然本质和组合数的方法一样,但想起来好像要容易很多……反正对本数学蒟蒻来说是这样的。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ld long double
using namespace std;
const int N = 1005;

int n, k, fz, fm;
ld a[N], p, ans[N];

int main(){
    cin >> n >> k;
    for(int i = 1; i <= n; i++){
        cin >> a[i];
        a[i + n] = a[i];
    }
    for(int i = n + 1; i <= n * 2; i++){
        p = (ld)k / (ld)n;
        fz = n - k;
        fm = n - 1;
        ans[i - n] = p * a[i];
        for(int j = 1; j <= n - k; j++){
            p = p * (ld)fz / (ld)fm;
            ans[i - n] += p * a[i - j];
            fz--;
            fm--;
        }
    }
    for(int i = 1; i <= n; i++){
        if(i > 1) printf(" ");
        printf("%.12Lf", ans[i]);
    }
    return 0;
}

回到顶部


H. Height Profile

题意

平面上有n+1个点,它们的横坐标依次是从0到n的整数,从左到右依次给出每个点的纵坐标,然后把这些点从左到右顺次相连,形成一条折线。k次询问,每次给出一个斜率g(下面的代码里写成q了),问这条折线上是否存在两个点,它们的连线的斜率不小于g。如果有,求出这两个点的横坐标的最大差值;如果没有,输出-1。

解法

边画边想,容易想到 如果存在这样的两个点,那么它们至少有一个横坐标是整数[1]。首先考虑暴力解法:枚举每个横坐标为整数的点,分别找到 它左侧横坐标最小的、它右侧横坐标最大的 两个可行的横坐标为整数的点,如果这两个点不是第0个、第n个点,可以简单地用数学方法求得向左、向右最多能延伸到什么位置。然后更新答案即可。

[1] 假设两个端点横坐标都不是整数,也就是说它们都位于折线的某一段的中间。如果这两段的斜率相等,那找到的线段就可以 两个端点固定在折线上 任意平移。那我们把线段平移到可行的最左端或最右端位置,这种情况就相当于两个点横坐标都是整数。如果两段斜率不相等,那我们可以向上或向下移动这条线段,使得它长度变长且斜率不变,就不是最优解了。

可是上述做法时间复杂度能达到O(k·n2),会TLE。设上述过程中枚举到某个横坐标为r的点时,找到的左侧最远的可行点是l,就有(h[r]-h[l])/(r-l) ≥ g,也就是h[r]-rg ≥ h[l]-lg。可以把所有整数横坐标的点按h[i]-ig从小到大排序。从前往后遍历,枚举到横坐标为i的点时,已经遍历过的点中横坐标最小的点,就是左侧要找的点。直接更新答案即可。找右侧可行点同理。时间复杂度只有O(k·nlogn),可以通过。

代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
#include<set>
#define ll long long
#define ld long double
using namespace std;
const int N = 1e5 + 5;
const ll INF = 1e18;

int n, k;
ld h[N], q;
struct node{
    int id;
    ld mk;
    node(int id1 = 0, ld mk1 = 0){
        id = id1;
        mk = mk1;
    }
    bool operator < (const node &n1) const{
        if(mk == n1.mk) return id < n1.id;
        return mk < n1.mk;
    }
}d[N];

int main(){
    cin.tie(0);
    cin >> n >> k;
    for(int i = 0; i <= n; i++){
        cin >> h[i];
    }
    while(k--){
        cin >> q;
        q *= 10;
        ld ans = 0;
        for(int i = 0; i <= n; i++){
            d[i] = node(i, h[i] - (ld)i * q);
        }
        sort(d, d + 1 + n);
        int minpos = d[0].id;
        for(int i = 1; i <= n; i++){
            if(d[i].id < minpos){
                minpos = d[i].id;
                continue;
            }
            ld cur = (ld)(d[i].id - minpos);
            if(minpos > 0){
                cur += (h[d[i].id] - q * cur - h[minpos]) / (q - h[minpos] + h[minpos - 1]);
            }
            ans = max(ans, cur);
        }
        int maxpos = d[n].id;
        for(int i = n - 1; i >= 0; i--){
            if(d[i].id > maxpos){
                maxpos = d[i].id;
                continue;
            }
            ld cur = (ld)(maxpos - d[i].id);
            if(maxpos < n){
                cur += (h[maxpos] - q * cur - h[d[i].id]) / (q - h[maxpos + 1] + h[maxpos]);
            }
            ans = max(ans, cur);
        }

        if(ans == 0){
            printf("-1\n");
        }
        else{
            printf("%.12Lf\n", ans);
        }
    }
    return 0;
}

回到顶部


posted on 2025-06-16 23:14  C12AK  阅读(26)  评论(1)    收藏  举报