前言
第一次打 Div 1+2 ,三个小时的时长边吃夜宵边打完了。
总体感觉本次比赛和 Div 2 难度差别不大,但是还是打得太烂了。
题目链接:Dashboard - EPIC Institute of Technology Round Summer 2024 (Div. 1 + Div. 2) - Codeforces
A. Upload More RAM
略
#include<cstdio>
#include<cstring>
using namespace std;
int T,n,k;
int main()
{
scanf("%d",&T);
while (T --)
{
scanf("%d%d",&n,&k);
printf("%d\n",(n - 1) * k + 1);
}
return 0;
}
B. K-Sort
略
比赛的时候电脑卡一直编译不了,过了十多分钟才交上去
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 100005
int T,n,m;
long long a[N],mx,b[N],ans;
int main()
{
scanf("%d",&T);
while (T --)
{
scanf("%d",&n),m = 0,ans = 0ll;
for (int i = 1;i <= n;++ i) scanf("%lld",&a[i]);
mx = a[1];
for (int i = 1;i <= n;++ i)
if(a[i] < mx) b[++ m] = mx - a[i];
else mx = a[i];
sort(b + 1,b + m + 1),b[0] = 0ll;
for (int i = 1;i <= m;++ i) ans += 1ll * (m - i + 2) * (b[i] - b[i - 1]);
printf("%lld\n",ans);
}
return 0;
}
C. Basil's Garden
思维题,提供简单思路。
对于第 i 朵花:
1. 若其高度小于等于第 i+1 朵,则其完全倒下的时间为第 i+1 朵完全倒下的时间 +1
2. 若其高度大于第 i+1 朵,则其完全倒下的时间为 “第 i+1 朵完全倒下的时间 +1 ” 和 “其高度”之间的最大值。
其实是一道很简单的题,比赛时想得略微复杂,以下附上考场代码,仅供参考。
#include<cstdio>
#include<cstring>
using namespace std;
#define N 100005
int T,n,a[N];
long long ans,now;
long long max(long long x,long long y) { return x > y ? x : y ; }
int main()
{
scanf("%d",&T);
while (T --)
{
scanf("%d",&n),a[n + 1] = 0;
for (int i = 1;i <= n;++ i) scanf("%d",&a[i]);
ans = now = 1ll * a[n];
for (int i = n - 1; i ;-- i)
if(a[i + 1] >= a[i]) ++ now,ans = max(ans,now);
else
{
now = max(1ll * a[i] - 1ll * (now - a[i + 1] + 1),1ll * a[i + 1]) + 1ll * (now - a[i + 1] + 1);
ans = max(ans,now);
}
printf("%lld\n",ans);
}
return 0;
}
D. World is Mine
博弈论。
比赛到1个小时的时候想了个错误的贪心,一直过不了,于是直接休息去吃宵夜了。到比赛结束前 20min 的时候突然想到了正确的贪心,回到电脑前开敲,遗憾的是到比赛结束都没调出来,更搞笑的是恰恰就在比赛结束后1min 内猛然发现一个数组名打错了,改了之后在能继续提交的时候交上去就A了。。。
题意:
有两个人吃一排蛋糕,每个蛋糕都有一个美味值,Alice 吃的蛋糕的美味值只能随时间顺序 严格递增,而 Bob 可以随意选择蛋糕。由 Alice 先吃,再由 Bob 吃,每个人每轮只能吃一个蛋糕,交替进行,直到其中一个人无蛋糕可吃时停止。
Alice 会选择让自己吃到最多蛋糕的策略,Bob 会选择让 Alice 吃到最少蛋糕的策略,求游戏结束时 Alice 能吃到的最多的蛋糕数量。
思路:
看了很多人包括 Tutorial 都用的是 dp,那我就讲讲比赛时的贪心做法。(核心思路一致)
无疑问的是要先对所有蛋糕以美味值排个序。
首先很显然的是 Alice 的选择策略是确定的:即选择吃当前可选择的美味值最小的那块蛋糕。那么关键就在于 Bob 怎么选择。其实 Bob 的选择在某种意义上也可以说确定的,他的选择一定是在 Alice 当前可选择的蛋糕里,并且能把美味值相同的某种蛋糕全部取走才算 “有意义”,否则就是 “无意义” 的选择。贪心的思路是:如果 Bob 能选走所有美味值为 x 的蛋糕,记这些美味值为 x 的蛋糕数目为 m,则 Bob 必须能选走所有在 x 之后(美味值大于x)且数目小于 m 的蛋糕。
贪心的时候注意细节即可。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 5005
int T,n,m,a[N],ans,now,bz,id,num[N],tot[N],vis[N];
int check(int x)
{
if(x == m) return 1;
int tmp = 0;
int f = 1;
for (int i = x;i <= m;++ i)
if(tot[i] < tot[x] || i == x)
{
if(num[i] - tmp < tot[i])
{
f = 0;
break;
}
tmp += tot[i] + 1;
}
return f;
}
int main()
{
scanf("%d",&T);
while (T --)
{
memset(num,0,sizeof num);
memset(tot,0,sizeof tot);
memset(vis,0,sizeof vis);
scanf("%d",&n),m = ans = 0;
for (int i = 1;i <= n;++ i) scanf("%d",&a[i]);
sort(a + 1,a + n + 1),a[0] = a[n + 1] = 0;
for (int i = 1;i <= n;++ i)
if(a[i] != a[i - 1]) tot[++ m] = 1;
else ++ tot[m];
id = 0,bz = 1;
while (id <= m)
{
if(bz)
{
-- bz;
while (id <= m && (!id || vis[id])) ++ id;
if(id == m + 1) break;
vis[id] = 1;
++ ans;
}
else
{
memset(num,0,sizeof num);
int cnt = 0;
int flag = 0;
for (int i = id + 1;i <= m;++ i)
if(!vis[i]) num[i] = ++ cnt,flag = i;
if(!flag) break;
flag = 0;
for (int i = id + 1;i <= m;++ i)
if(num[i] >= tot[i] && check(i))
{
flag = i;
break;
}
if(!flag)
{
bz = m;
continue;
}
vis[flag] = 1;
bz = tot[flag];
}
}
printf("%d\n",ans);
}
return 0;
}
E. Wonderful Tree!
这题感觉相较于D还没那么难想。
我们可以从下至上解决每个节点的问题,即可以在 dfs 回溯的时候解决,对于每个不满足要求的节点,我们只能通过增加其儿子的值来满足要求。
那么问题是当有多个儿子存在时,到底要增加哪个儿子的值呢?假设某个儿子的值为 val,这个儿子的儿子们的价值之和为 sum,如果 val < sum,那么可以放心地把这个儿子的 val 增加 1 的代价;可如果所有儿子的 val 都大于等于 sum,也就是说增值任意一个儿子都会导致要接着增值这个儿子的儿子,以此类推。
我们可以定义一个数组 表示在 i 的子树内,深度(以根节点的深度是1为标准)为 j 的那一层可 “放心使用” 的增值代价。这里 “放心使用” 的意思就是,在增值了 i 到第 j 层的某条路径上的节点后,不会对 j 以下的节点造成影响。那么十分显然,对于所有的叶子节点,这个代价可以是无穷大的。那么对于我们目前 dfs 到的某个节点 x ,从dep[x] + 1层到最底下那层扫过去,一层一层填补 x 节点需要的差值。设与 x 深度差为 dif 的那层填补了 k 的差值,那么这一层的总贡献就是k * dif (因为从 x 到这层的某一条路径上每个点都要增值)。在回溯的时候将 x 儿子们的 rem 归并到 x 身上即可。
#include<cstdio>
#include<cstring>
using namespace std;
#define N 5005
int T,n,st[N],cnt;
long long ans,a[N],dif[N],rem[N][N];
struct Edge
{
int next,to;
}ed[N];
long long min(long long x,long long y) { return x < y ? x : y ; }
void add(int u,int v)
{
ed[++ cnt].next = st[u];
ed[cnt].to = v;
st[u] = cnt;
return;
}
void dfs(int x,int dep)
{
if(!x) return;
dif[x] -= a[x];
for (int i = st[x]; ~i ;i = ed[i].next)
{
int rec = ed[i].to;
dfs(rec,dep + 1),dif[x] += a[rec];
for (int j = 1;j <= n;++ j) rem[x][j] += rem[rec][j];
}
if(st[x] == -1) dif[x] = 1e13;
if(dif[x] >= 0) rem[x][dep] += dif[x];
else
{
long long now = -dif[x];
for (int i = dep + 1;i <= n;++ i)
{
long long tmp = min(rem[x][i],now);
now -= tmp,rem[x][i] -= tmp,ans += 1ll * (i - dep) * tmp;
if(!now) break;
}
}
return;
}
int main()
{
scanf("%d",&T);
while (T --)
{
scanf("%d",&n),ans = 0ll,cnt = 0;
for (int i = 1;i <= n;++ i) scanf("%lld",&a[i]),st[i] = -1,dif[i] = 0ll;
for (int i = 2,x;i <= n;++ i) scanf("%d",&x),add(x,i);
for (int i = 1;i <= n;++ i)
for (int j = 1;j <= n;++ j)
rem[i][j] = 0ll;
dfs(1,1);
printf("%lld\n",ans);
}
return 0;
}
F. Interesting Problem (Easy Version) && (Hard Version)
一道有意思的 区间dp 题。
Easy Version 的做法是设 表示在 i 之前消了 k 对时,区间 [i,j] 能消的最多数目,最后的答案就是
,时间复杂度是 O(n^4)。
Hard Version:
由于序列中的数都是以相邻成对的形式消去的所以从最终的结果看来应该形式如下:
(...)oo(...)(...)o(...)o
括号内的意思是括号内的数字都被消去了,括号外的 o 就是未被消去的意思,这给我们的启发是每一组最大的括号之间都是互不重叠的,于是可以迫使我们想到 dp 状态更新贡献。
我们可以设 表示区间 [1,i] 最大可消除的数目,那么只需统计出每一段 “大括号” 是否可以完全消去就能更新状态了。因此还需要处理一个状态
表示区间 [i,j] 要想完全消去,在 i 之前必须要消去的数字对数的最小值。假设我们想要删位置 x 上的数(假设 x 是主动者),而在 x 之前已经删了 t 对,那么必须满足 x - 2 * t = a[x] 且 x 和 a[x] 的奇偶性相同,即 t = (x - a[x]) / 2 。
对于当前要更新的区间 [l,r] ,要想删除位置 l 上的数,有以下两种情况:
1. ((...))(...)
2. ((...))
设与位置 l 上的数一同删去的是位置 k 上的数,对于这两种情况,一定都要满足 l,k 之间 (...) 的 f 值小于等于 t 的值才可以更新状态(否则就无法删去l)。
对1:f[l][r] = min(f[l][r],max(t,f[k + 1][r] - (k - l + 1) / 2))
对2:f[l][r] = min(f[l][r],t)
综上所述,求 f 的复杂度是O(n^3),求 ans 的复杂度是O(n^2),总复杂度是O(n^3)。
#include<cstdio>
#include<cstring>
using namespace std;
#define N 805
int T,n,a[N],f[N][N],ans[N];
int min(int x,int y) { return x < y ? x : y ; }
int max(int x,int y) { return x > y ? x : y ; }
void solve()
{
for (int i = 0;i <= n;++ i)
for (int j = 0;j <= n;++ j) f[i][j] = 0x3f3f3f3f;
for (int i = 0;i <= n;++ i) ans[i] = f[i][i] = 0;
for (int i = 1;i < n;++ i)
if((i >= a[i]) && (!((i - a[i]) & 1))) f[i][i + 1] = (i - a[i]) >> 1;
for (int len = 4;len <= n;len += 2)
for (int l = 1;l + len - 1 <= n;++ l)
{
if(((l - a[l]) & 1) || (l < a[l])) continue;
int r = l + len - 1;
int x = (l - a[l]) >> 1;
for (int k = l + 1;k <= r;k += 2)
if(f[l + 1][k - 1] <= x || k == l + 1)
f[l][r] = min(f[l][r],max(x,f[k + 1][r] - (k - l + 1) / 2));
if(f[l + 1][r - 1] <= x) f[l][r] = min(f[l][r],x);
}
for (int r = 1;r <= n;++ r)
{
ans[r] = ans[r - 1];
for (int l = r - 1;l > 0;l -= 2)
if(f[l][r] <= ans[l - 1])
ans[r] = max(ans[r],ans[l - 1] + (r - l + 1) / 2);
}
printf("%d\n",ans[n]);
return;
}
int main()
{
scanf("%d",&T);
while (T --)
{
scanf("%d",&n);
for (int i = 1;i <= n;++ i) scanf("%d",&a[i]);
solve();
}
return 0;
}
总结
本场比赛 DP 题居多,暴露了自己太久没打 DP 而不熟练的短板,得练。同时 D 题就晚了1分钟,也是给自己敲响了警钟,比赛尤其到最后时刻一旦有想法绝不能手慢,这次 D 题虽然是最后20min才想到正确解法,但还是一边聊天一边慢慢悠悠地打,继续加油吧。