妙妙题大合集。
T1
令 \(dp_{i,j}\) 表示分离出以 \(i\) 为根的恰含 \(j\) 节点的树所需的最小删边数。
有初始状态 \(dp_{i,1}=\) 其子节点个数,其余为 \(\infty\)。
对于答案,我们考虑到对于每个节点 \(i\),除了其子树内的删边数之外,它的父节点与它的连边也应删去(注意根节点 \(root\) 无需考虑)。
于是每个节点的答案为
\[\begin{cases} dp_{i,p}+1 \ \ \ i \neq root \\ dp_{i,p} \ \ \ i=root \end{cases} \]对所有节点的答案取 \(\min\) 即为最终答案。
对于转移,我们需要保留根节点与其儿子的连边,即要删除的边少一条。
于是有转移
\[dp_{x,v}=\min(dp_{x,v},dp_{i,k}+dp_{x,v-k}-1) \](\(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\),则有转移方程
\[dp_i=dp_{pre-1}+1 \]这个转移方程的实质即为将以 \(pre-1\) 结尾的合法括号串与当前的括号串拼接在一起,形成了一种新的合法括号串,在加上以 \(pre-1\) 结尾的 \(dp_{pre-1}\) 种合法括号串,就得到了 \(dp_i\)。
然后,我们只需要用一个栈维护 \(pre\) 即可。
将这个序列问题搬到树上做,就成了本题。
具体地,我们仍令 \(dp_i\) 表示以 \(i\) 结尾的合法括号串数量。
根据 \(dp_i\) 的定义可知,\(k_i=\sum^i_{j=1} dp_j\)。
仍然考虑状态转移。
我们令与当前右括号匹配的左括号的位置为 \(pre\),则有转移方程
\[dp_i=dp_{fa_{pre}}+1 \]注意此处 \(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;
}
T3
本题使用朴素选边 / 枚举点对 + LCA 求距离 的方式,以 \(O(C_{n}^{k} \times n^2 \log n)\) 的优秀时间复杂度,即可拿到 \(0\) 分的好成绩。
考虑如何进行优化。
我们想到,对于每一条边,计算它被那哪些点对间的路径经过,累加贡献即可。
具体地,我们令 \(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;
}