Living-Dream 系列笔记 第53期

妙妙题大合集。

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;
}

作业 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;
}
posted @ 2024-04-13 21:42  _XOFqwq  阅读(2)  评论(0编辑  收藏  举报