9月14-20日小记 & 校运动会游记 & CSP-S1 2025 游记
标题可能有点长,但是我想说的都被写到这篇博客里了,请认真读下去吧。
对我来说,9月14-22日是这一月中意义非凡的一段时间,可能是因为学习了树形数据结构,可能是因为和同学们一起去了新校区,在偌大的校园里逛了很久,可能是因为在新校区的机房里和好友颓了一天,可能是因为CSP赛场上的灵光一现,可能是因为考完试在公交车上看到了晚霞红遍天空……
9月14日
基本没干什么,忙于赶文化课作业的进度。
着手了一部新番,《地——关于地球的运动》。虽然都是疯人院的,但是感觉剧本质量明显逊于同时期的《葬送的芙莉莲》,可能是制作组人选的问题吧。人物文本很幼稚,大篇幅的描写花在了毫无用处的地方,在该强调的地方笔墨反而少了很多。情节走向很老套。但OP和ED都超级好听!同时吹爆山口和拿不拿
准备在接下来的一周里学习树形数据结构。同时做了一道题。
1. P4913 【深基16.例3】二叉树深度
左右孩子式存二叉树。由于二叉树是递归定义的,所以我们在计算深度的过程中也要使用DFS来递归。
由题意知,二叉树的深度是指从根节点到叶子结点时,最多经过了几层。所以我们只需要从根不断DFS,找到叶子节点的层数中的最深者即可。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=2e5+7;
struct Node{
int left,right;
}tree[N];
int n;
int DFS(int x){
if(x==0)return 0;
return max(DFS(tree[x].left),DFS(tree[x].right))+1;
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>tree[i].left>>tree[i].right;
}
cout<<DFS(1);
return 0;
}
9月15日
1. P1827 [USACO3.4] 美国血统 American Heritage
题意简述:给出一棵二叉树的中序遍历和前序遍历,求后序遍历。
板子。定义一个递归函数,传这棵二叉树的中序遍历和前序遍历区间,在函数中按照“左子树--右子树--根”的遍历顺序输出后序遍历。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
string a,b;
int n;
void build(int l1,int r1,int l2,int r2){
//l1-r1:先序遍历
//l2-r2:中序遍历
for(int i=l2;i<=r2;i++){
if(a[i]==b[l1]){
build(l1+1,l1+i-l2,l2,i-1);
build(l1+i-l2+1,r1,i+1,r2);
cout<<b[l1];
return;
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>a>>b;
a=' '+a,b=' '+b;
int n=a.length()-1;
build(1,n,1,n);
return 0;
}
2. P4715 【深基16.例1】淘汰赛
据题意模拟即可。将输入的所有数字化为二叉树的叶子结点,两两相邻构成一组来建树。两节点的父节点决策由这两节点的值大小决定。最后输出的是亚军,请注意。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=260;
int tree[N],win[N],n;
void DFS(int x){
if(x>=(1<<n)){
return;
}
DFS(2*x);
DFS(2*x+1);
int l=tree[2*x],r=tree[2*x+1];
if(l>r){
tree[x]=l;
win[x]=win[2*x];
}else{
tree[x]=r;
win[x]=win[2*x+1];
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
for(int i=0;i<(1<<n);i++){
cin>>tree[i+(1<<n)];
win[i+(1<<n)]=i+1;
}
DFS(1);
cout<<(tree[2]>tree[3]?win[3]:win[2]);
return 0;
}
3. P5076 【深基16.例7】普通二叉树(简化版)
二叉搜索树的板子,待AC。
晚上头晕的要死,眼前时常重影,没上晚自习,去了医大一做血常规检查。
9月16日
上午去了一个私人医院,在群力新区,做了核磁,大受震撼。中午吃了拉面。丽江路上,寒风刺骨。
回到学校的第一刻就奔向钢琴而去。
在机房,一位OIer打开了他尘封多年的一本通OJ。
1. 1336:【例3-1】找树根和孩子
树的一个很重要的特性:一个节点可以有很多儿子,但一个儿子不能有好几个爹。这一特性在后面的树形DP转移中也有很广泛的应用。
由此我们可以用数组t[i]
来记录第i
个节点的爹是哪个节点,由此实现遍历。然后就是一个模拟了。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=110;
int n,m,t[N],root,maxn,maxi;
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n>>m;//n--节点数 m--边数
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
t[y]=x;
}
for(int i=1;i<=n;i++){
if(t[i]==0){
root=i;
break;
}
}
for(int i=1;i<=n;i++){
int sum=0;
for(int j=1;j<=n;j++){
if(t[j]==i){
sum++;
}
}
if(sum > maxn){
maxn=sum;
maxi=i;
}
}
cout<<root<<'\n'<<maxi<<'\n';
for(int i=1;i<=n;i++){
if(t[i]==maxi){
cout<<i<<' ';
}
}
return 0;
}
2. 1337:【例3-2】单词查找树
【补充:这玩意好像叫Trie树,在CSP-S1 2025试卷中出现了。考试时我还不知道什么是Trie树,于是想到了这道题,没想到蒙对了】
挺好的一道题。
考虑对所有单词进行字典序排序。观察发现,对于排序后相邻的两个单词s[i]
与s[i-1]
而言,所需在树上增加的节点数,恰好等于s[i]
的长度减去两单词相同部分的长度j
。
由于根节点没被计算,所以要把输出结果加1
。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=1e5+10;
string s[N];
int n,ans;
signed main(){
cin.tie(0)->sync_with_stdio(0);
string S;
while(cin>>S){
s[++n]=S;
}
sort(s+1,s+1+n);
for(int i=1;i<=n;i++){
int j;//j--两单词相同部分的长度
for(j=0;j<n&&s[i][j]==s[i-1][j];j++);
ans += s[i].length()-j;
}
cout<<ans+1;
return 0;
}
3. 1338:【例3-3】医院设置(朴素做法)
重拾最短路。
考虑将树看做一个图,跑Floyd最短路。边权均为1。最后计算结果的时候,将每个除正在枚举的节点以外的所有节点与正在枚举的节点之间的最短路乘上以外的节点的点权并累计答案,统计最小值即可。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=110;
int n,ans=INT_MAX,a[N],g[N][N];
signed main(){
cin.tie(0)->sync_with_stdio(0);
memset(g,0x3f,sizeof g);
cin>>n;
for(int i=1;i<=n;i++){
int l,r;
g[i][i]=0;
cin>>a[i]>>l>>r;
if(l)g[i][l]=g[l][i]=1;
if(r)g[i][r]=g[r][i]=1;
}
for(int k=1;k<=n;k++){//Floyd的断点
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
}
}
}
for(int i=1;i<=n;i++){
int tot=0;
for(int j=1;j<=n;j++){
tot += g[i][j] * a[j];
}
ans=min(ans,tot);
}
cout<<ans;
return 0;
}
4. 1339:【例3-4】求后序遍历
同美国血统。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
string a,b;
void build(int l1,int r1,int l2,int r2){
int tag=b.find(a[l1]);
if(tag>l2)build(l1+1,l1+tag-l2,l2,tag-1);
if(tag<r2)build(l1+tag-l2+1,r1,tag+1,r2);
cout<<a[l1];
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>a>>b;
int n=a.length()-1;
build(0,n,0,n);
return 0;
}
5. 1363:小球(drop)
有趣的模拟题。似乎有性质可以实现\(O(1)\)?
考虑模拟每个小球的运动轨迹。当小球走到某个节点时,将其的点权异或上1
。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=524295;
bool b[N];
int d,k,ans;
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>d>>k;
int p=1;
for(int i=1;i<=k;i++){//遍历小球
p=1;
while(p<(1<<(d-1))){
b[p]^=1;
if(b[p]==1){
p=2*p;
}else{
p=2*p+1;
}
}
if(i==k)ans=p;
}
cout<<ans;
return 0;
}
6. 1364:二叉树遍历(flist)
板子。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
string a,b;
int n;
void build(int l,int r){
int j;
for(int i=0;i<=n;i++){
for(j=l;j<=r;j++){
if(a[j]==b[i]){
cout<<a[j];
goto L;
}
}
}L:
if(j>l){
build(l,j-1);
}if(j<r){
build(j+1,r);
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>a>>b;
n=a.length()-1;
build(0,n);
return 0;
}
9月17日
1. P1229 遍历问题
好玩的Trick。
根据前序和后序遍历,只能精准确定任一节点的层数(可能表述不准确)。对于每个只有一个子节点的节点,其子节点可能在左或右。所以只需统计所有符合上述性质的节点,输出它关于2的次幂即可。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
string a,b;
int n,ans;
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>a>>b;
n=a.length();
for(int i=0;i<n;i++){
for(int j=1;j<n;j++){
if(a[i]==b[j] && a[i+1]==b[j-1]){
ans++;
}
}
}
cout<<(1<<ans);
return 0;
}
2. P1087 [NOIP 2004 普及组] FBI 树
递归定义即可。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n;
string s;
char DFS(int l,int r){
if(l==r){
if(s[l]=='0'){
cout<<'B';
return 'B';
}else{
cout<<'I';
return 'I';
}
}
int mid=l+r>>1;
char L=DFS(l,mid),R=DFS(mid+1,r);
if(L=='B' && R=='B'){
cout<<'B';
return 'B';
}else if(L=='I' && R=='I'){
cout<<'I';
return 'I';
}else{
cout<<'F';
return 'F';
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n>>s;
s=' '+s;
DFS(1,1<<n);
return 0;
}
3. P1030 [NOIP 2001 普及组] 求先序排列
板子。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
string a,b;
void build(int l1,int r1,int l2,int r2){
cout<<b[r2];
int tag=a.find(b[r2]);
if(tag>l1)build(l1,tag-1,l2,r2-r1+tag-1);
if(tag<r1)build(tag+1,r1,l2+tag-l1,r2-1);
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>a>>b;
int n=a.length()-1;
build(0,n,0,n);
return 0;
}
4. P1305 新二叉树
根据输入方式,要用左右孩子式存二叉树。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=130;
int n;char root;
struct Node{
char l,r;
}t[N];
void pre(char x){
if(x=='*')return;
cout<<x;
pre(t[x].l);
pre(t[x].r);
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
char c;cin>>c;
if(i==1)root=c;
cin>>t[c].l>>t[c].r;
}
pre(root);
return 0;
}
5. P3884 [JLOI2009] 二叉树问题
读了好久才读明白题意。
不放按如下方式建图:将每个向上指的边的权都设为2,将每个向下指的边的权都设为1。跑Floyd即可。
至于宽度,可以开一个桶,存某一层上点的个数,找最大值即可。深度之前写过,不说了。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=105;
int n,g[N][N];
unordered_map<int,int>layer;
signed main(){
cin.tie(0)->sync_with_stdio(0);
memset(g,0x3f,sizeof g);
cin>>n;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
g[x][y]=1,g[y][x]=2;
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
g[i][j]=min(g[i][j],g[i][k]+g[k][j]);//Floyd最短路,和医院题类似,可以把树看作一个图,但可能有更好的做法
}
}
}
int x,y;cin>>x>>y;
int maxs=0,maxd=0,maxn=1;
for(int i=2;i<=n;i++){
maxs=max(maxs,g[1][i]);
maxn=max(maxn,g[i][1]);
layer[g[1][i]]++;//跑桶,计数
}
for(int i=1;i<=maxn;i++){
maxd=max(maxd,layer[i]);
}
cout<<maxs+1<<'\n'<<maxd<<'\n'<<g[x][y];
return 0;
}
6. P5908 猫猫和企鹅
深搜的板子。你甚至不需要认为它是一棵树。
对于每个节点,搜索与它距离在d
之内的所有节点的个数。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=1e5+10;
int n,d,ans;
vector<int>g[N];
bool vis[N];
void DFS(int x,int depth){
vis[x]=1;
if(depth==d)return;
for(int i=0;i<g[x].size();i++){
if(!vis[g[x][i]]){
DFS(g[x][i],depth+1);
ans++;
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n>>d;
for(int i=1;i<n;i++){
int x,y;cin>>x>>y;
g[x].push_back(y);
g[y].push_back(x);
}
DFS(1,0);
cout<<ans;
return 0;
}
9月18日
实在想不起来那天做什么了……
9月19日 & 校运动会游记
9月18日晚上,新校区机房使用权限的申请被通过。这使得今天我与dyx、lyd在机房待了一天,而并没有参加运动会。
树形DP大学习。
上午dyx和lyd在颓麻将,下午也在颓。值得注意的是,dyx在颓麻将的过程中切了一道紫。
lyd由于前一天晚上忙于准备送给初中老师的礼物,一宿没睡,导致今天身体症状频发,睡眠欲明显变强。
购买了yyy提供的24N2班刊,很好看。
新校区的小卖部简直就是超市!买了两瓶红茶。
运动会闭幕式时和dyx讨论了一道题(待议),目前无解。
运动会结束了,学校的大门开了,下午四点多钟,金黄色的阳光穿过运动场,洒在每个奔跑着的少年的脸上。
晚上去初中学校看望了物理老师。然后在松雷楼上吃饭碰到了很多熟人,包括他。我想起她了。不如就此终结吧。
1. P1352 没有上司的舞会
十分经典的树形DP。
让我们回顾树形结构:每个节点都有且仅有一个父亲,但可能有多个孩子。这启发我们要用孩子的状态转移父亲的状态。
对于这道题而言,每个节点只有选或不选两个操作,可以开两个数组记录状态。
f0[i]
表示不选i
节点情况下,i
节点及其子树所能选到的最大快乐指数;f1[i]
表示选i
节点情况下,i
节点及其子树所能选到的最大快乐指数。那么我们可以开f
数组的第二维,将两个数组压缩为一个,便于操作。
根据题意,如果节点i
被选,其子节点一定不能被选,故状态转移方程显然:
f[i][0] = Σmax(f[j][0],f[j][1])
,
f[i][1] = Σf[j][0]
,
其中j
是i
的子节点。
边界是f[i][1] = r[i]
。
答案是max(f[root])
。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=6e3+10;
vector<int>g[N];
int r[N],n,root,f[N][2];
bool flag[N];
void DFS(int i,int father){
for(auto j:g[i]){
if(j==father)continue;//避免回到父节点,导致无限搜索
DFS(j,i);
f[i][0] += max(f[j][0],f[j][1]);
f[i][1] += f[j][0];
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>f[i][1];
}
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
flag[x]=1;
// g[x].push_back(y);//搜索是单向的,不用建双向边
g[y].push_back(x);
}
for(int i=1;i<=n;i++){
if(!flag[i]){
root=i;
break;
}
}
DFS(root,-1);//根的父节点不存在
cout<<max(f[root][0],f[root][1]);
return 0;
}
2. P2015 二叉苹果树
(1) 朴素做法
树上DP。
定义f[i][j]
表示以i
为根的子树,保留j
根树枝,能留住的最多苹果数。
状态转移方程:
f[i][x+y+2]=max(f[i][x+y+2],f[l][x]+f[r][y]+g[i][l]+g[i][r])
f[i][x+1]=max(f[i][x+1],f[l][x]+g[i][l])
f[i][y+1]=max(f[i][y+1],f[r][y]+g[i][r])
其中g[i][j]
表示两节点间的边权,l
、r
分别是i
的左、右子节点编号;x
从0
到以l
为根的子树边数siz[l]
遍历,y
从0
到以r
为根的子树边数siz[r]
遍历。
答案显然是f[1][q]
。根据题意,1
总为根节点。
值得注意的是,虽然必须当i
到j
间有边时才建树,但是如果边权为0
时,根据以前的判断逻辑,会认为它没边,所以我们将g
数组赋一个极大初值,如果g
小于极大初值,那么证明有边,可以建树。否则79pts
。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=105;
int n,q,g[N][N],f[N][N],siz[N],l[N],r[N];
void build(int i,int father){//建树,预处理左右节点
for(int j=1;j<=n;j++){
if(g[i][j]<0x3f3f3f3f && j!=father){
if(!l[i])l[i]=j;
else if(!r[i])r[i]=j;
build(j,i);
}
}
}
void DFS(int i,int father){//预处理子树节点/边数,并递推
siz[i]=0;
if(l[i]){
DFS(l[i],i);
siz[i] += siz[l[i]]+1;
}
if(r[i]){
DFS(r[i],i);
siz[i] += siz[r[i]]+1;
}
for(int x=0;x<=siz[l[i]];x++){
for(int y=0;y<=siz[r[i]];y++){
f[i][x+1]=max(f[i][x+1],f[l[i]][x]+g[i][l[i]]);
f[i][y+1]=max(f[i][y+1],f[r[i]][y]+g[i][r[i]]);
f[i][x+y+2]=max(f[i][x+y+2],f[l[i]][x]+f[r[i]][y]+g[i][l[i]]+g[i][r[i]]);
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
memset(g,0x3f,sizeof(g));
cin>>n>>q;
for(int i=1;i<n;i++){
int u,v,w;
cin>>u>>v>>w;
g[u][v]=g[v][u]=w;
}
build(1,-1);
DFS(1,-1);
cout<<f[1][q];
return 0;
}
(2) 背包思想
显然对于每个子树上的节点,只有选或不选两种选择,这就是一个多重背包,打板子即可。
【需要确认】
注:结构化绑定OI考试用不了
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=105;
int n,q,f[N][N],siz[N];
vector<pair<int,int>>t[N];
void DFS(int u,int father){
for(auto&[v,val]:t[u]){//结构化绑定
if(v==father)continue;
DFS(v,u);
siz[u] += siz[v]+1;
for(int j=min(siz[u],q);j;j--){
for(int k=0;k<=min(siz[v],j-1);k++){
f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+val);
}
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n>>q;
for(int i=1;i<n;i++){
int u,v,val;
cin>>u>>v>>val;
t[u].push_back(make_pair(v,val));
t[v].push_back(make_pair(u,val));
}
DFS(1,-1);
cout<<f[1][q];
return 0;
}
3. P2014 [CTSC1997] 选课
树形DP,和舞会题很类似。
定义f[i][j]
表示以i
为根的子树上,选j
个节点,能取得的最大学分。
我们发现,对于一个子树而言,一个根的孩子会影响根的状态转移,所以我们考虑从根的孩子开始进行转移。
对于一个节点u
,使用邻接矩阵存树,遍历其孩子v
,状态转移方程是:
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k])
其原理类似分组/多重背包DP,意思是:以节点u
为根的子树上选j
个节点,相当于以节点u
的各个孩子v
为根的子树上选k
个节点的状态值,加上以节点u
为根的子树上选j-k
个节点的状态值。
在输入时,a[i]
的值实际上就是f[i][1]
的值,没必要单开一个数组记录a[i]
。
关于虚根优化:我们可以构造一个编号为0
的虚根,来统领一片森林。虚根是假想的、但必须选择的课程,学分为0
,所以变量m
在输入时应自增1
。
单向边优化:为了防止由反向状态转移导致的无限DFS
,我们建从x
到i
的单向边。但要注意,当状态转移支持反相转移时,单项边优化存在弊端。
可能的解决方案是:建双向边,但在DFS中传两个参量,其中一个是正在搜索的起始节点,另一个是它爹,用于在遍历起始节点能够到达的节点时,防止搜到它爹。
code:
#include <bits/stdc++.h>
using namespace std;
constexpr int N=310;
int f[N][N],n,m;
map<int,vector<int>>g;
void DFS(int u){
for(auto&v:g[u]){
DFS(v);
for(int j=m;j;j--){
for(int k=0;k<j;k++){
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
}
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n>>m;
m++;//虚根
for(int i=1;i<=n;i++){
int x;
cin>>x>>f[i][1];
g[x].push_back(i);//建单向边
}
DFS(0);
cout<<f[0][m];
return 0;
}
4. P1122 最大子树和
比舞会好。
注意到本题数据范围很大,启示我们不能开二维DP数组。
定义f[u]
表示以u
为根的子树上所能选出的最大权值。
显然对于u
的每一个子节点v
,当f[v]
为负时,其贡献状态转移没有积极作用,所以当且仅当f[v]
为正时,我们才进行状态转移。
转移方程非常显然:f[u]+=f[v]
,当且仅当f[v]>0
。
尽量不要使用单向边优化,否则容易被HACK,原因已经在上面说明
答案要遍历每一个f[i]
,找出最大值。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=16010;
int n,ans=INT_MIN,root,a[N],f[N];
bool flag[N];
map<int,vector<int>>g;
void DFS(int u,int father){
for(auto&v:g[u]){
if(v==father)continue;
DFS(v,u);
if(f[v]>0){
f[u]+=f[v];
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
memset(f,0xc0,sizeof(f));
cin>>n;
for(int i=1;i<=n;i++){
cin>>f[i];
}
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
DFS(1,-1);
for(int i=1;i<=n;i++){
ans=max(ans,f[i]);
}
cout<<ans;
return 0;
}
5. P8625 [蓝桥杯 2015 省 B] 生命之树
上面一道题的双倍经验。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=100010;
int n,ans=-1e18,root,a[N],f[N];
bool flag[N];
map<int,vector<int>>g;
void DFS(int u,int father){
for(auto&v:g[u]){
if(v==father)continue;
DFS(v,u);
if(f[v]>0){
f[u]+=f[v];
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
memset(f,0xc0,sizeof(f));
cin>>n;
for(int i=1;i<=n;i++){
cin>>f[i];
}
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
DFS(1,-1);
for(int i=1;i<=n;i++){
ans=max(ans,f[i]);
}
cout<<(ans<0?0:ans);
return 0;
}
6. P1270 “访问”美术馆
切了一道蓝,令人满意。
根据题图,不难发现这是一个树形DP。
这道题的输入有点意思,是递归定义的。我们在输入一个节点u
的相关信息时,会引出其两个子节点的输入指令,所以我们写一个输入函数,在这个函数的末尾,递归两次输入指令。
定义f[i][j]
表示节点i
及其子树上,拿了j
幅画,所需要的最少时间。
显然对于每一个节点u
,有f[u][x+y]=min(f[u][x+y],f[l][x]+f[l][y]+2*(cost[l]+cost[r]))
,其中l
、r
表示u
的左、右节点,cost[l/r]
表示走节点l
或r
所需的时间。
至于cost[l]+cost[r]
需要乘2
的原因,是左右子树需要往返遍历,每个通道均走了两次。
值得注意的是,当x
或y
为0
时,表示某一子树不选,此时对应的cost
为0
,可以使用三目运算符实现。
边界:根据题意,对于每个叶子节点u
,都有f[u][i]=i*5
;搜索时搜到叶子节点应当return
。
当u==1
时,表示当前走的是根节点,没有父边,只需要走单程,特判一下即可。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=610;
int n=1,ans,limit,f[N][N],v[N];
struct Node{
int ind,tim;
};
vector<Node>g[N];
void read(int u){
int x,y;
cin>>x>>y;
g[u].push_back({++n,x});
if(y){
v[n]=y;
return;
}
int z=n;
read(z);read(z);
}
void DFS(int u){
if(g[u].empty()){
for(int i=0;i<=v[u];i++){
f[u][i]=i*5;
}
return;
}
if(u==1){
DFS(g[u][0].ind);
f[u][0]=0;
for(int i=1;i<=600;i++){
f[u][i]=f[g[u][0].ind][i]+g[u][0].tim*2;
}
return;
}
DFS(g[u][0].ind);
DFS(g[u][1].ind);
for(int x=0;x<=600;x++){
for(int y=0;x+y<=600;y++){
int costl=x?g[u][0].tim:0;
int costr=y?g[u][1].tim:0;
f[u][x+y]=min(f[u][x+y],f[g[u][0].ind][x]+f[g[u][1].ind][y]+2*(costl+costr));
}
}
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
memset(f,0x3f,sizeof(f));
cin>>limit;
read(1);
DFS(1);
for(int i=1;i<=600;i++){
if(f[1][i]<limit){
ans=i;
}
}
cout<<ans;
return 0;
}
7. B4090 [CSP-X2020 山东] 侠盗阿飞
lyd的神秘问题,贪心,优先帮助钱数少的即可。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=5e4+10;
int ans,tot,w,n,a[N];
signed main(){
cin.tie(0)->sync_with_stdio(0);
cin>>w>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+1+n);
for(int i=1;i<=n;i++){
tot+=a[i];
if(tot<=w)ans++;
else break;
}
cout<<ans;
return 0;
}
8. P2618 数字工程
lyd的神秘线性DP。
首先筛出数据范围内所有数字的素因子,然后按照题意转移即可。
转移方程是f[i]=f[i-1]+1
和f[i]=f[i/j]+1
,取最小值,其中j
是i
的素因子。
转移时,转移数据范围内所有数的f
数组值,在输出时直接输出即可。有点打表预处理的思想。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N=1e6+10;
int n,f[N];
vector<int>fct[N];
bool vis[N];
signed main(){
cin.tie(0)->sync_with_stdio(0);
for(int i=2;i<=1e6;i++){
if(vis[i])continue;
for(int j=i;j<=1e6;j+=i){
vis[j]=1;
fct[j].push_back(i);
}
}
for(int i=2;i<=1e6;i++){
f[i]=f[i-1]+1;
for(auto&j:fct[i]){
f[i]=min(f[i],f[i/j]+1);
}
}
while(cin>>n)cout<<f[n]<<'\n';
return 0;
}
9月20日 & CSP-S1 2025 游记
早上起床后很不精神,买了一杯瑞幸,发现瑞幸和崩铁联动了,令人忍俊不禁。
上午板刷CSP-S1历年真题。刷着刷着就困了,于是开始颓扫雷。刚开始还想着破PB,但运气特别不好,没有胜局。
中午弹琴,接着颓扫雷。
13:05,十人乘地铁出发。在出行的过程中,他们聊了很多,我却什么也没说,因为我想听歌。
13:55左右到达德强学校,在校门口碰到了X编程学校的F老师在大声直播,内容大概就是说学OI的优势啊,他们学校如何如何好啊之类的。
F似乎看出了十人是三中的,于是上去搭话,问了dyx很多问题,并对这些问题的答案下了几个不太正确的归总性结论,好像dyx是他们学校教出来的一样。
F用来直播的桌子上放着几个所谓“模板代码”的招生宣传卡,旁边有一些助教在散发它们。yyy忍不住拿了一个,上面的LIS竟然是\(O(n^2)\)的。下面放着F的肖像,旁边配了一些文字,大概是“X学校专业做编程”之类的意思。
考场门口有很多家长,都在忙于谈论孩子们的OI和whk生涯。
大约14:10,进入考场。注意到我和lzm、wzh、zhw、lyb同一考场。
考试于14:30开始。
先做单选。卧槽什么是Trie树?可能是之前做过的一本通上的单词查找树?手模了一下,好像真是。
卧槽拓扑排序又是啥?时间复杂度题显然有陷阱。还好单选中好像只有拓扑排序的概念不太清楚,其余题还都真不太难。
然后就做完善程序。
T1题目叫特殊最短路,启示我们要用Dijkstra算法。由于我在考场上不知道怎么想的,把Dijkstra算法的存在彻底忘却了,于是手贪了一下。发现没啥问题,就开始填了。挺顺利的。
T2也不是很难。抽象数据接口的概念很新颖。其实与T2类似的问题,我在隔壁机房讲信息论的时候却已听说过了。但是prev_permutation
是个啥?
最后做阅读程序。
T1显然是全排列的变式,要求生成的排列中没有相邻的正序对。
T2是根号分块(但是我在考场上看guess2
函数时没看懂,随便蒙了一个二分法,就错了)。You have no egg!\n
引人遐想。
T3没做,交给CCF了。
试卷结束,用时1h40min,最后检查出来一个单选,竟然是Trie树推错了,少看了一个节点。
1h55min左右,望向窗外,浅灰色的云随着风飘去,我的思绪亦向远方奔腾。
收卷。结束了。hkb在路上跟我说,他要炸了。
估分,76.5pts。我很满意,又很不满意,错了很多手模的东西。
晚上去了中央大街。回到家开始赶物理作业,发现自己啥也不会,于是开始复健。复健了两个多小时,发觉已是十一点多了,于是赶忙躺下睡觉了。
哦对了,今天听到了一首很好听的歌,歌名叫《某种信念》。
我此刻应当也正怀着某种信念吧。