2025.2.15做题记录
前言
只有当教练让你补题的时候,你才会知道你之前多摆。
————麦塞尔夫
补题内容为:树形dp 和 区间dp
联合权值
观察题面并注意到,对于一个点只有孙子爷爷和兄弟会产生联合权值,自然想到对于一个点 \(u\) ,记录它的父亲 \(fa\) 遍历它的儿子 \(v\) 那么产生的联合权值为 \(w_{fa}\times w_v\) 与 \(w_v\times sum\)。 \(sum\) 为儿子的和。
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
#define int long long
using namespace std;
const int N=2e5+50;
const int mod=1e4+7;
vector<int> e[N];
int n,w[N];
int mxans,ans;
void dfs(int u,int fa){
int mx=0,sum=0;
for(int i=0;i<e[u].size();i++){
int v=e[u][i];
if(v==fa) continue;
dfs(v,u);
mxans=max(mxans,max(w[fa]*w[v],w[v]*mx));
ans=(ans%mod+w[fa]*w[v]%mod)%mod;
ans=(ans%mod+sum*w[v]%mod)%mod;
mx=max(mx,w[v]);
sum+=w[v];
}
return ;
}
signed main(){
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
for(int i=1;i<=n;i++){
cin>>w[i];
}
dfs(1,0);
cout<<mxans<<' '<<ans*2%mod;
return 0;
}
最大子树和
读题观察到对于每一株花有保留与不保留两种情况,设 \(dp\) 数组为第 \(u\) 株花 保留/不保留(1/0) 的情况。
发现只有当第 \(u\) 株花有贡献时,才会保留(你不会保留一个负贡献的垃圾)。那么就做完了。最后发现 \(dp{_u}{_,}{_0}\) 没用,可以省略不写。
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=2e4+50;
vector<int> e[N];
int w[N],n,ans=-2147483647;
int f[N][3];
void dfs(int u,int fa){
f[u][1]=w[u];
for(int i=0;i<e[u].size();i++){
int v=e[u][i];
if(v==fa) continue;
dfs(v,u);
f[u][0]=0;
if(f[v][1]>0)
f[u][1]+=f[v][1];
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>w[i];
}
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
memset(f,0xcf,sizeof(f));
dfs(1,0);
for(int i=1;i<=n;i++){
ans=max(ans,f[i][1]);
}
cout<<ans;
return 0;
}
二叉苹果树
同上感觉比较裸。考虑定义 \(dp\) 数组为:对于第 \(u\) 个节点保留 \(j\) 根树枝的情况。依题意就可得状态转移方程:\(dp{_u}{_,}{_j}=\max(dp{_u}{_,}{_j},dp{_u}{_,}_{j-k-1}+dp{_v}{_,}{_k})\)
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=200;
struct Edge{
int v,w;
};
vector<Edge> e[N];
int n,q,f[N][N],rt[N];
void dfs(int u,int fa){
for(int i=0;i<=q;i++) f[u][i]=rt[u];
for(int i=0;i<e[u].size();i++){
int v=e[u][i].v,w=e[u][i].w;
if(v==fa) continue;
rt[v]=w;
dfs(v,u);
for(int j=q;j>=0;j--){
for(int k=0;k<j;k++){
f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]);
}
}
}
}
int main(){
cin>>n>>q;
for(int i=1;i<n;i++){
int u,v,w;
cin>>u>>v>>w;
e[u].push_back({v,w});
e[v].push_back({u,w});
}
dfs(1,0);
cout<<f[1][q];
return 0;
}
染色
神秘好题。考虑将 从 \(i\) 到 \(j\) 的区间刷成目标颜色,如果 \(i\) 与 \(j\) 相同颜色,那么可以直接从 \(i\) 到 \(j-1\) 的状态继承(我多画一点怎么你了?)。如果 \(i\) 与 \(j\) 不同颜色,那么考虑枚举一个断点使得左右总花费和最小。
有一种神秘想法就是把整个过程反过来,考虑一堆颜色叠在一起往上取,最终把所有颜色取完。可能对于上述过程更好理解(反正我是OvO)。
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
string s;
int f[60][60];
int main(){
cin>>s;
s=' '+s;
int l=s.length()-1;
for(int i=1;i<=l;i++){
f[i][i]=1;
}
for(int len=2;len<=l;len++){
for(int i=1;i+len-1<=l;i++){
int j=i+len-1;
if(s[i]==s[j]) f[i][j]=f[i][j-1];
else{
int mid=2e9;
for(int k=i;k<j;k++){
mid=min(f[i][k]+f[k+1][j],mid);
}
f[i][j]+=mid;
}
}
}
cout<<f[1][l];
return 0;
}
248 G
板题。枚举 \(i\) 到 \(j\) 区间断点 \(k\) 使 \(i\) 到 \(k\) 区间合成的最大值与 \(k+1\) 到 \(j\) 合成的最大值相同。
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=300;
int n,a[N];
int f[N][N];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
f[i][i]=a[i];
}
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
for(int k=i;k<j;k++){
if(f[i][k]==f[k+1][j] && f[i][k]>0){
f[i][j]=max(f[i][j],f[i][k]+1);
}
}
}
}
int ans=-1;
for(int len=1;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
ans=max(ans,f[i][j]);
}
}
cout<<ans;
return 0;
}
合唱队
另一种板题,考虑左侧右侧插入的条件,并考虑从左侧或右侧转移。
#include<iostream>
using namespace std;
int f[2010][2010][2],a[2010],n;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
f[i][i][0]=1;
}
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
if(a[i]<a[i+1]) f[i][j][0]+=f[i+1][j][0];
if(a[i]<a[j]) f[i][j][0]+=f[i+1][j][1];
if(a[j]>a[i]) f[i][j][1]+=f[i][j-1][0];
if(a[j]>a[j-1]) f[i][j][1]+=f[i][j-1][1];
f[i][j][0]%=19650827;
f[i][j][1]%=19650827;
}
}
cout<<(f[1][n][0]+f[1][n][1])%19650827;
return 0;
}
调整队形
观察题目发现添加人和踢掉人效果相同。(我们添加一个人使左右对称,为什么不把影响左右对称的人踢出去?)那么就只需要考虑踢人与换衣服。
对于两人衣服颜色相同,直接继承 \(i+1,j-1\) 。
对于两人衣服颜色不同:
对于踢人,可以从 \(i+1,j\) 与 \(i,j-1\) 处转移。
对于换衣服,可以从\(i+1,j-1\) 处转移。(将 \(i\) 的衣服颜色换成 \(j\) 即可)。
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=3e3+50;
int n,a[N];
int f[N][N];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
if(a[i]==a[j]) f[i][j]=f[i+1][j-1];
else{
f[i][j]=min(f[i+1][j-1],min(f[i+1][j],f[i][j-1]))+1;
// 改变衣服颜色 踢掉i人 踢掉j人
}
}
}
cout<<f[1][n];
return 0;
}
加分二叉树
本题可视为区间dp的良好练手题。
因为题目给出的为中序遍历,所以对于一个由我们钦定的节点,它的左右区间就是他的左右子树。那么可以设置 \(dp\) 数组的意义为:区间 \(l\) 到 \(r\) 的最大得分。
那么接下来遍历区间 \(l\) 到 \(r\) ,寻找一个断点 \(k\) ,使得 \(dp{_l}{_,}{_{k-1}} \times dp_{k+1}{_,}{_{r}}+a_k\) 最大。
预处理 \(l=r\) 的情况(将其视为叶子节点)为 \(dp{_l}{_,}{_r}=a_l\) 与 \(l=r-1\) 的情况(空子树为1)为 \(dp{_l}{_,}{_r}=1\)。
接下来就是对于前序遍历,这个就很简单了。在对上述区间做 dp 时,记录 \(l\) 到 \(r\) 区间的根节点,最后递归输出即可。
AC code:
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
#define int long long
using namespace std;
int n,a[40];
int f[40][40];
int root[40][40];
void print(int l,int r){
if(l>r) return ;
cout<<root[l][r]<<' ';
if(l==r) return ;
print(l,root[l][r]-1);
print(root[l][r]+1,r);
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
f[i][i]=a[i];
f[i][i-1]=1;
root[i][i]=i;
}
for(int len=2;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
f[i][j]=f[i+1][j]+a[i];
root[i][j]=i;
for(int k=i+1;k<j;k++){
if(f[i][j]<f[i][k-1]*f[k+1][j]+a[k]){
f[i][j]=f[i][k-1]*f[k+1][j]+a[k];
root[i][j]=k;
}
}
}
}
cout<<f[1][n]<<endl;
print(1,n);
return 0;
}

浙公网安备 33010602011771号