1584 ZJOI2008 骑士(Bzoj1040 LOJ10162 LUOGU2607 省选/NOI-) 枚举组合数30分 类01背包树上DP10分 基环树有向图(强制不选某点正确性证明)100分

本文探讨了如何利用深度优先搜索(DFS)解决涉及憎恨关系的组合问题,从初始的错误思路到逐步改进,最终通过基环树和有向图表示,解决了树形结构下的100分题目。编码过程包括邻接矩阵、邻接表和删除多余选择的策略。
摘要由CSDN通过智能技术生成

总目录

在线测评地址(ybt)

在线测评地址(LOJ)

在线测评地址(LUOGU)

没什么思路,不过n=10这30分可以试试。

此时的深搜是输出所有的组合数,能编得出吗,尝试了,代码如下。

#include <bits/stdc++.h>
#define maxn 1010
using namespace std;
int a[maxn],vis[maxn],n;
void dfs(int step,int len){
	if(step==len+1){
		for(int j=1;j<=len;j++)
			printf("%d ",a[j]);
		printf("\n");
		return;
	}
	for(int i=1;i<=n;i++)
		if(!vis[i]){
			vis[i]=1;
			a[step]=i;
			dfs(step+1,len);
			vis[i]=0;
		} 
}
int main(){
	int i;
	scanf("%d",&n);
	for(i=1;i<=n;i++)
		dfs(1,i);
	return 0;
}

对应的输入输出数据如下:

输入:
3
输出:
1
2
3
1 2
1 3
2 1
2 3
3 1
3 2
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

看了效果,发现写成了输出所有排列数,马上改进,还是要输出所有组合数。

#include <bits/stdc++.h>
#define maxn 1010
using namespace std;
int a[maxn],n;
void dfs(int step,int len){
	if(step==len+1){
		for(int j=1;j<=len;j++)
			printf("%d ",a[j]);
		printf("\n");
		return;
	}
	for(int i=a[step-1]+1;i<=n;i++){
		a[step]=i;
		dfs(step+1,len);
	} 
}
int main(){
	int i;
	scanf("%d",&n);
	for(i=1;i<=n;i++)
		dfs(1,i);
	return 0;
}

对应的输入输出数据如下:

输入:
3
输出:
1
2
3
1 2
1 3
2 3
1 2 3

现在可以开始编写  对于 30% 的测试数据,满足n≤10 的情况了

1.枚举所有组合数的深搜30分

ybt

未通过

测试点结果内存时间
测试点1答案正确640KB2MS
测试点2答案正确652KB2MS
测试点3答案正确656KB2MS
测试点4运行超时788KB995MS
测试点5运行超时820KB998MS
测试点6运行超时980KB998MS
测试点7运行错误592KB2MS
测试点8运行错误584KB1MS
测试点9运行错误584KB1MS
测试点10运行错误584KB1MS

LOJ

LUOGU

该编码,比较有开创性的是,用邻接矩阵hate[][]描述两者憎恨关系。

#include <bits/stdc++.h>
#define maxn 1010
#define LL long long
using namespace std;
int a[maxn],n,hate[maxn][maxn];
LL b[maxn],mx;
void dfs(int step,int len){
	if(step==len+1){
		LL sum=0;
		int i,j;
		for(i=1;i<=len;i++){
			int k=a[i];
			for(j=1;j<i;j++)	
				if(hate[k][a[j]])//排查与之前选中骑士的关系 
					return;
			sum+=b[k];
		}
		mx=max(mx,sum);
		return;
	}
	for(int i=a[step-1]+1;i<=n;i++){
		a[step]=i;
		dfs(step+1,len);
	} 
}
int main(){
	int i,v;
	scanf("%d",&n);
	for(i=1;i<=n;i++){
		scanf("%lld%d",&b[i],&v);
		hate[i][v]=hate[v][i]=1;
	}
	for(i=1;i<=n;i++)
		dfs(1,i);
	printf("%lld\n",mx);
	return 0;
}

2.类01背包的树上DP10分

通过上述30分编码,发现,憎恨关系是树形关系,可以用邻接表来表示,这是才发现该题与树是有关系的。

样例中的憎恨关系如下图所示:

测试数据2

输入:
3
50 3
20 3
30 1
输出:
70

上述数据中的憎恨关系如下图:

 要是有个节点数量多些的数据该有多好,这样更能发现规律。

突然有个想法,若是树形结构,该题就比较好解决,若是有回路,如样例,该题暂时没有办法,先解决树形结构。

v是u的子节点

f[u][0]+=max(f[v][1],f[v][0])

v选了,u必不能选。

v不选,u可选,可不选。

f[u][1]+=f[v][0];

u选了,v必不能选。

根怎么定,直觉告诉我,谁作根都可以。

预判,应该可以拿到50分。

编码过程中,突然发现,相互憎恨的去重,也挺麻烦。

那么退而求其次,为了去重,开一个vis[][]二维数组,那么数据处理降到 对于60% 的测试数据,满足n≤100

 测试数据2 很快通过,提交

ybt

未通过

测试点结果内存时间
测试点1运行错误8808KB5MS
测试点2答案正确704KB2MS
测试点3运行错误8848KB5MS
测试点4运行错误9172KB10MS
测试点5运行错误9248KB6MS
测试点6答案错误1408KB2MS
测试点7运行错误592KB1MS
测试点8运行错误584KB1MS
测试点9运行错误584KB1MS
测试点10运行错误580KB1MS

LOJ

  

LUOGU

 10分代码如下:

#include <bits/stdc++.h>
#define maxn 1010
#define LL long long
using namespace std;
LL f[maxn][2];
int n;
int head[maxn],tot,vis[maxn][maxn];
struct node{
	int to,next;
}e[maxn<<1];
void add(int u,int v){
	tot++,e[tot].to=v,e[tot].next=head[u],head[u]=tot;
}
void dfs(int u,int fa){
	int i,v;
	for(i=head[u];i;i=e[i].next){
		v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		f[u][0]+=max(f[v][1],f[v][0]);
		f[u][1]+=f[v][0];
	}
}
void init(){
	int u,v;
	scanf("%d",&n);
	for(u=1;u<=n;u++){
		scanf("%lld%d",&f[u][1],&v);
		if(!vis[u][v]&&!vis[v][u]){
			add(u,v),add(v,u);
			vis[u][v]=vis[v][u]=1;
		}
	}
}
int main(){
	init();
	dfs(1,-1);
	printf("%lld\n",max(f[1][0],f[1][1]));
	return 0;
} 

3.基环树

如果它不是有n条边,而是n-1条边的话,它就是一棵树,也就是  没有上司的舞会。
现在只多了一条边,那它就是基环树。相当于多了一个环,其他的结构没有很大的变化。

组成的是基环树森林而非单棵基环树

有向图 避免了 无向图中出现的重边。

所以 考虑这个有向图

我们把x所讨厌的人y设为x的父亲节点(理由是:父节点只有一个,父节点的子节点有多个)

题中样例对应的有向图

每个联通块内有且只有一个简单环

这样 我们考虑把每个联通块的环上删一条边

这样它必然构成树

然后要注意

删掉的边所连接的两点x,y

是不能同时选的

所以我们分别强制x,y其中一个点不选,这个一定不选的f[][1]=-maxn.

删边,并不是真正意义上的删边,是通过节点值的设置,不选该点。

难点:为什么两遍深搜结果取最大值是正确的(强制不选某点正确性证明)?

证明如下:

x存在选,不选两种可能。y存在选,不选两种可能。
组合后有4种可能。
x选,y选
x选,y不选
x不选,y选
x不选,y不选

因x,y两节点是互斥的,只有3种可能的组合是正确的。
x选,y不选
x不选,y选
x不选,y不选

x作为根,强制不选,那么包含2种可能的组合。
x不选,y选
x不选,y不选

y作为根,强制不选,那么包含2种可能的组合。
x选,y不选
x不选,y不选

将 x作为根,强制不选,y作为根,强制不选,合并后,包含3种可能的组合。
x选,y选
x不选,y不选
x不选,y不选

与 因x,y两节点是互斥的,只有3种可能的组合是正确的。完全匹配。
x选,y不选
x不选,y选
x不选,y不选

故在删边过程中,分别强制设置两个端点不选,结果是正确的。

样例的删边模拟过程如下:

根节点root=3,根节点的父节点fa[root]=1,删除1->3这条边

选节点3作为整棵树的根,强制节点3不能选(遍历到节点3时,设置f[3][1]=-maxn,注意设置一个很大的负数)。
f[2][0]=0,f[2][1]=20
f[1][0]=0,f[1][1]=10
f[3][0]=0,f[3][1]=-maxn

f[1][0]+=max(f[3][0],f[3][1]),f[1][0]=0
f[1][1]+=f[3][0],f[1][1]=10
f[2][0]+=max(f[1][0],f[1][1]),f[2][0]=10
f[2][1]+=f[[1][0],f[2][1]=20
f[3][0]+=max(f[2][0],f[2][1]),f[3][0]=20
f[3][1]+=f[2][0],f[3][1]=-maxn

t=0
t=max(f[3][0],f[3][1])
t=20


选节点1作为整棵树的根,强制节点1不能选(遍历到节点1时,设置f[1][1]=-maxn,注意设置一个很大的负数)。
f[3][0]=0,f[3][1]=30
f[2][0]=0,f[2][1]=20
f[1][0]=0,f[1][1]=-maxn


f[2][0]+=max(f[1][0],f[1][1]),f[2][0]=0
f[2][1]+=f[1][0],f[2][1]=20
f[3][0]+=max(f[2][0],f[2][1]),f[3][0]=20
f[3][1]+=f[[2][0],f[3][1]=30
f[1][0]+=max(f[3][0],f[3][1]),f[1][0]=30
f[1][1]+=f[3][0],f[1][1]=-maxn

t=max(t,max(f[1][0],f[1][1])),t=30





对新树跑DP

显然相邻的点是不能选的

所以得到状态转移方程:

用f[i][0/1]表示以i为根节点的子树选i/不选i所能获得的最大价值

则 f[i][0]=sigema(max(f[son][0],f[son][1])); for each son of i

f[i][1]=sigema(f[son][0]); for each son of i

ybt

通过

测试点结果内存时间
测试点1答案正确644KB1MS
测试点2答案正确664KB2MS
测试点3答案正确648KB2MS
测试点4答案正确656KB2MS
测试点5答案正确652KB2MS
测试点6答案正确660KB2MS
测试点7答案正确804KB2MS
测试点8答案正确1164KB4MS
测试点9答案正确15956KB129MS
测试点10答案正确50868KB539MS

LOJ

LUOGU

 基环树有向图找环删边100分代码如下:

#include <bits/stdc++.h>
#define maxn 1000010
#define LL long long
using namespace std;
int n,head[maxn],tot,fa[maxn],vis[maxn],save;
LL f[maxn][2],mx,c[maxn];
int root;
struct node{
	int to,next;
}e[maxn<<1];
void add(int u,int v){
	tot++,e[tot].to=v,e[tot].next=head[u],head[u]=tot;
}
void init(){
	int v,u;
	scanf("%d",&n);
	for(v=1;v<=n;v++){
		scanf("%lld%d",&c[v],&u);
		add(u,v);
		fa[v]=u;
	}
}
void dfs(int u){
	int i,v;
	f[u][1]=c[u],f[u][0]=0;//两次dfs对应的初始化 
	for(i=head[u];i;i=e[i].next){
		v=e[i].to;
		vis[v]=1;
		if(v==root){
			f[v][1]=-maxn;
			continue;
		}
		dfs(v);
		f[u][1]+=f[v][0];
		f[u][0]+=max(f[v][0],f[v][1]);
	}
}
void find_circle(int x){
	LL t=0;
	vis[x]=1;
	root=x;
	while(!vis[fa[root]]){
		root=fa[root];
		vis[root]=1;
	}
	//将root与fa[root]连线隔断。
	dfs(root);
	t=max(f[root][0],f[root][1]);
	root=fa[root];
	dfs(root);
	t=max(t,max(f[root][0],f[root][1]));
	mx+=t;//基环树森林处理
}
int main(){
	init();
	for(int i=1;i<=n;i++)
		if(!vis[i])//基环树森林处理 
			find_circle(i); 
	printf("%lld\n",mx);
	return 0;
} 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值