Living-Dream 系列笔记 第53期
妙妙题大合集。
T1
令 \(dp_{i,j}\) 表示分离出以 \(i\) 为根的恰含 \(j\) 节点的树所需的最小删边数。
有初始状态 \(dp_{i,1}=\) 其子节点个数,其余为 \(\infty\)。
对于答案,我们考虑到对于每个节点 \(i\),除了其子树内的删边数之外,它的父节点与它的连边也应删去(注意根节点 \(root\) 无需考虑)。
于是每个节点的答案为
对所有节点的答案取 \(\min\) 即为最终答案。
对于转移,我们需要保留根节点与其儿子的连边,即要删除的边少一条。
于是有转移
(\(x\) 为当前根,\(v\) 为背包容量,\(i\) 为 \(x\) 的儿子,\(k\) 为 \(i\) 的子树内留下的节点数,下同)
code
#include<bits/stdc++.h> using namespace std; const int N=2e2+5; int n,p; int dp[N][N]; vector<int> G[N<<1]; int dfs(int x){ int siz=1; dp[x][1]=G[x].size(); for(int i:G[x]){ int son=dfs(i); siz+=son; for(int v=min(siz,p);v>0;v--) for(int k=0;k<=min(v-1,son);k++) dp[x][v]=min(dp[x][v],dp[i][k]+dp[x][v-k]-1); } return siz; } int main(){ memset(dp,0x3f,sizeof(dp)); cin>>n>>p; for(int i=1,u,v;i<n;i++) cin>>u>>v, G[u].push_back(v); dfs(1); int ans=dp[1][p]; for(int i=2;i<=n;i++) ans=min(ans,dp[i][p]+1); cout<<ans; return 0; }
T2
首先考虑一个弱化版问题:
给定一个括号串 \(t\),定义 \(s_i\) 为 \(t_{1 \sim i}\),对于所有的 \(i\),若 \(s_i\) 中有 \(k_i\) 个互不相同的子串为合法括号串,求 \(k_i\)。
对于该问题,我们令 \(dp_i\) 表示以 \(i\) 结尾的合法括号串数量。
根据 \(dp_i\) 的定义可知,\(k_i=\sum^i_{j=1} dp_j\)。
考虑状态转移。
首先 \(dp_i\) 发生状态转移当且仅当 \(t_i\) 为右括号,因为只有在此时才会发生左右括号匹配。
我们令与当前右括号匹配的左括号的位置为 \(pre\),则有转移方程
这个转移方程的实质即为将以 \(pre-1\) 结尾的合法括号串与当前的括号串拼接在一起,形成了一种新的合法括号串,在加上以 \(pre-1\) 结尾的 \(dp_{pre-1}\) 种合法括号串,就得到了 \(dp_i\)。
然后,我们只需要用一个栈维护 \(pre\) 即可。
将这个序列问题搬到树上做,就成了本题。
具体地,我们仍令 \(dp_i\) 表示以 \(i\) 结尾的合法括号串数量。
根据 \(dp_i\) 的定义可知,\(k_i=\sum^i_{j=1} dp_j\)。
仍然考虑状态转移。
我们令与当前右括号匹配的左括号的位置为 \(pre\),则有转移方程
注意此处 \(pre\) 的前一个并非 \(pre-1\),而是 \(fa_{pre}\)。
我们同样采用栈维护 \(pre\) 即可。
不同的是,树上的每个左括号都可能有多个右括号与之匹配,而在序列上是唯一的。
于是我们考虑找完一条路径后就回溯。
具体而言,我们用一个标记 \(f\) 标记当前的右括号是(\(1\))否(\(0\))被匹配。
在回溯时,若 \(f=1\),则将其配对的左括号重新进栈;
其余的左括号则均应将其从栈内弹出。
然后这题就做完了。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=5e5+5; int n; int dp[N],fa[N]; string s; vector<int> G[N]; stack<int> stk; void dfs(int x){ int pre,f=0; dp[x]=0; if(s[x]=='(') stk.push(x); else if(!stk.empty()) pre=stk.top(),stk.pop(),f=1, dp[x]=dp[fa[pre]]+1; for(int i:G[x]) dfs(i); if(f) stk.push(pre); else if(s[x]=='(') stk.pop(); } void getsum(int x){ for(int i:G[x]) dp[i]+=dp[x],getsum(i); } signed main(){ cin>>n>>s,s="#"+s; for(int i=2;i<=n;i++) cin>>fa[i],G[fa[i]].push_back(i); dfs(1),getsum(1); int ans=0; for(int i=1;i<=n;i++) ans^=(i*dp[i]); cout<<ans; return 0; }
作业 T1
我们想到,对于每一条边,计算它被那哪些点对间的路径经过,累加贡献即可。
具体地,我们令 \(dp_{i,j}\) 表示以 \(i\) 为根的子树内有 \(j\) 个黑点时的最大贡献。
答案显然为 \(dp_{1,k}\)。
对于初始状态,有 \(dp_{i,0}=dp_{i,1}=0\)(因为这两种情况始终合法),其余均为 \(-\infty\)。
对于状态转移,我们将子树外的黑点数 \(\times\) 子树内的黑点数 \(\times\) 边权即为该边对黑点的贡献。白点同理。
注意开 long long
并建双向边即可。
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e3+5; int n,k; int dp[N][N]; struct Edge{ int to,w; }; vector<Edge> G[N]; int dfs(int x,int f){ int siz=1; dp[x][0]=dp[x][1]=0; for(Edge i:G[x]){ if(i.to==f) continue; int son=dfs(i.to,x); siz+=son; for(int v=min(siz,k);v>=0;v--){ for(int p=0;p<=min(son,v);p++){ int black=p*(k-p)*i.w; int white=(son-p)*(n-k-son+p)*i.w; dp[x][v]=max(dp[x][v],dp[i.to][p]+dp[x][v-p]+black+white); } } } return siz; } signed main(){ memset(dp,0xcf,sizeof(dp)); cin>>n>>k; for(int i=1,u,v,w;i<n;i++) cin>>u>>v>>w, G[u].push_back({v,w}), G[v].push_back({u,w}); dfs(1,-1); cout<<dp[1][k]; return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!