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;
}
浙公网安备 33010602011771号