9.28 p.m.小结

T1:问题 A: [SCOI2005]扫雷

题目描述

相信大家都玩过扫雷的游戏。那是在一个n×mn\times mn×m的矩阵里面有一些雷,要你根据一些信息找出雷来。万圣节到了,“余”人国流行起了一种简单的扫雷游戏,这个游戏规则和扫雷一样,如果某个格子没有雷,那么它里面的数字表示和它8连通的格子里面雷的数目。现在棋盘是n×2n\times 2n×2的,第一列里面某些格子是雷,而第二列没有雷,如下图:

由于第一列的雷可能有多种方案满足第二列的数的限制,你的任务即根据第二列的信息确定第一列雷有多少种摆放方案。

输入

第一行为N,第二行有N个数,依次为第二列的格子中的数。(1<= N <= 10000)

输出

一个数,即第一列中雷的摆放方案数。

样例输入

2
1  1

样例输出

2

题解

第一眼看到这个题,我还以为是找雷的个数(也差不多)。既然只问种类数不问排列情况,那么直接DP上手。Dp[i][p][q]表示此刻枚举到第i个位置是不是雷,p=0表示不是,p=1表示是雷;同理,q=1表示第i-1个位置是雷,q=0表示不是。

显然,如果一个位置i的右边是0,那么前后三个位置都是0(表示没有雷)。

因此对于下一个位置有:dp[i+1][0][0]=dp[i][0][0],如果取其他情况就无法满足前后三个没有雷。

同理,对于一个位置i的右边是3,那么转移方程只有:dp[i+1][1][1]=dp[i][1][1]。

对于位置i右边为2,转移方程:

①dp[i+1][1][0]=dp[i][0][1]。②dp[i+1][1][1]=dp[i][1][0]。③dp[i+1][0][1]=dp[i][1][1]

对于位置为1.转移方程为:

①dp[i+1][1][0]=dp[i][0][0]。②dp[i+1][0][1]=dp[i][1][0]。③dp[i+1][0][0]=dp[i][0][1]

可以观察到dp[i+1][p][q]与转移的dp[i][x][y]一定存在以下关系:

X=q;p+q+y=a[i](a[i]表示第i个位置右边的数)。

因此枚举p,q,判断是不是符合上述条件,然后枚举完最后一层后,单独判断最后一层是不是已经满足条件(同样枚举p,q检验最后一个位置右边的数)。

当然,这整个过程都是线性的,所以答案不会很大。

参考代码

#include<cstdio>
using namespace std;
int dx[5]={0,0,0,1,1};
int dy[5]={0,0,1,0,1};
int n,dp[20000][5][5],a[20000],ans=0;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) 
	{
		scanf("%d",&a[i]);
		if(a[i]<0||a[i]>3) 
		{
			printf("0");
			return 0;
		}
	}
	for(int i=0;i<=1;i++)
	  for(int j=0;j<=1;j++)
		  if(i+j==a[1])
			dp[2][i][j]=1;
	for(int i=3;i<=n;i++)
	{
		if(a[i-1]==1)
		{
			dp[i][1][0]=dp[i-1][0][0];
			dp[i][0][1]=dp[i-1][1][0];
			dp[i][0][0]=dp[i-1][0][1];
		}
		else if(a[i-1]==2)
		{
			dp[i][0][1]=dp[i-1][1][1];
			dp[i][1][1]=dp[i-1][1][0];
			dp[i][1][0]=dp[i-1][0][1];
		}
		else if(a[i-1]==3)
		{
			dp[i][1][1]=dp[i-1][1][1];
		}
		else if(a[i-1]==0)
		{
			dp[i][0][0]=dp[i-1][0][0];
		}
	}
	for(int i=0;i<=1;i++)
	  for(int j=0;j<=1;j++)
	    if(i+j==a[n])
	      ans+=dp[n][i][j];
	printf("%d",ans);
	return 0;
}

T2:问题 B: 互不侵犯

题目描述

在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。

输入

只有一行,包含两个数N,K ( 1 <=N <=9, 0 <= K <= N * N)

输出

所得的方案数

样例输入

3 2

样例输出

16

题解

如果没记错,已经是第三次做这个状压了(难怪做的那么顺)。首先枚举每一个二进制状态,先看本身是否冲突。就是对于状态i,二进制下不能有2个1相邻,可以用(i&(i<<1))是否为0来判断。同时用lowbit处理出一个二进制数1的个数。再看DP部分,dp[p][i][k]表示第p行用了第i个状态,一共用去棋子数为k的所有方案数。转移方程很简单,对于每一行,枚举上一状态j,判断i与j是否合法(与之前类似,(i&(j<<1)),(i&j),((i<<1)&j)是否都为0),然后枚举棋子数并且更新方案数。

最后的答案是第n行所有状态,所用棋子数为k的方案数总和(要开long long)。

参考代码

#include<cstdio>
#define LL long long
using namespace std;
int n,k,lg[1025],cnt=0,lis[1025];
LL dp[10][1025][100],ans=0;
int getsum(int k)
{
	int ret=0;
	while(k)
	{
		ret++;
		k-=k&-k;
	}
	return ret;
}
int main()
{
	scanf("%d%d",&n,&k);
	if(n==1)
	{
		if(k<=1) printf("1");
		else printf("0");
		return 0;
	}
	for(int i=0;i<(1<<n);i++)
	{
		if((i&(i<<1))!=0) continue;
		lis[++cnt]=i;
		lg[cnt]=getsum(i);
		dp[1][i][lg[cnt]]=1ll;
	}
	for(int i=2;i<=n;i++)
	  for(int r=1;r<=cnt;r++)
	    for(int l=1;l<=cnt;l++)
	      if(((lis[l]&lis[r])==0)&&((lis[l]&(lis[r]<<1))==0)&&(((lis[l]<<1)&lis[r])==0))
			for(int nus=lg[r];nus<=k;nus++)
		      dp[i][lis[r]][nus]+=dp[i-1][lis[l]][nus-lg[r]];  
	for(int i=1;i<=cnt;i++)
	ans+=dp[n][lis[i]][k];
	printf("%lld",ans);
	return 0;
	        
}

T3:问题 C: 繁忙的都市

题目描述

城市C是一个非常繁忙的大都市,城市中的道路十分的拥挤,于是市长决定对其中的道路进行改造。城市C的道路是这样分布的:城市中有n个交叉路口,有些交叉路口之间有道路相连,两个交叉路口之间最多有一条道路相连接。这些道路是双向的,且把所有的交叉路口直接或间接的连接起来了。每条道路都有一个分值,分值越小表示这个道路越繁忙,越需要进行改造。但是市政府的资金有限,市长希望进行改造的道路越少越好,于是他提出下面的要求:

1.改造的那些道路能够把所有的交叉路口直接或间接的连通起来。 2.在满足要求1的情况下,改造的道路尽量少。 3.在满足要求1、2的情况下,改造的那些道路中分值最大的道路分值尽量小。

任务:作为市规划局的你,应当作出最佳的决策,选择那些道路应当被修建。

输入

第一行有两个整数n,m表示城市有n个交叉路口,m条道路。

接下来m行是对每条道路的描述,u, v, c表示交叉路口u和v之间有道路相连,分值为c。(1≤n≤300,1≤c≤10000,1≤m≤100000)

输出

两个整数s, max,表示你选出了几条道路,分值最大的那条道路的分值是多少。

样例输入

4 5
1 2 3
1 4 5
2 4 7
2 3 6
3 4 8

样例输出

3 6

题解

这道题可以猜想,所求的就是最小生成树。我是因为举了大概20个例子,发现没有反例,于是就这么写了。最后的结果也说明这个想法是正确的。至于如何理解,网上找了找也没看到(可能是我太菜了),因此可以想象求最小生成树的过程:小根堆和并查集,用了贪心的思想,每次取的必然是当前状态最小的边。如果改一改肯定不是更小的,总和会增加一个自然数,由于此刻选了个更低的(低于原来最小生成树的最长边),因此如果全部都小于该最长边,那么总和会变小,与题意不符,因此至少有一条边大于原来的最小边(或者等于,甚至是另一条最小生成树)。讲的不是很清楚,但是举100个例子应该会让人茅塞顿开的。

参考代码

#include<cstdio>
#include<queue>
using namespace std;
struct node
{
	int val,from,to;
};
priority_queue<node>q;
bool operator < (node m,node n)
{ return m.val>n.val; }
int n,m,fa[10001],ans=-1;
int find(int v)
{ return fa[v]==v?v:fa[v]=find(fa[v]); }
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		node hr;
		scanf("%d%d%d",&hr.from,&hr.to,&hr.val);
		q.push(hr);
	}
	for(int i=1;i<=n;i++) fa[i]=i;
	while(!q.empty())
	{
		node pt=q.top();q.pop();
		int u=find(pt.from),v=find(pt.to);
		if(u==v) continue;
		if(ans<pt.val) ans=pt.val;
		fa[u]=v; 
	}
	printf("%d %d",n-1,ans);
	return 0;
}

T4:问题 D:最大子矩阵

题目描述

这里有一个n*m的矩阵,请你选出其中k个子矩阵,使得这个k个子矩阵分值之和最大。注意:选出的k个子矩阵不能相互重叠。

输入

第一行为n,m,k(1≤n≤100,1≤m≤2,1≤k≤10),接下来n行描述矩阵每行中的每个元素的分值(每个元素的分值的绝对值不超过32767)。

输出

只有一行为k个子矩阵分值之和最大为多少。

样例输入

3 2 2
1 -3
2 3
-2 3

样例输出

9

题解

这道题可以划分成两个子问题,m=1和m=2。

先来看简单的m=1的情况。这就变成了从n个数中选择k段连续子序列使得总和最大。这样可以用简单的dp解决。首先预处理出前缀和sum,令dp[i][j]表示前i个数中取了j段,那么设置一个断点k,表示第j段是从k+1到i。最后在所以的状态中取一个最大值,并且往后枚举,如果说不取这个数反而更优,就可以直接转移:dp[i][j]=max(dp[i][j],dp[i-1][j])。最后的答案是dp[n][k]。

解决完m=1的情况,100分就拿了9分了,great!现在来看m=2的情况,完全可以看成两列,需要预处理出两列的前缀和sum[i][1]和sum[i][2],然后仿照m=1的方法,定义dp[i][j][p]表示第一列取了i个,第二列取了j个,共取了p个矩阵。那么同样可以不取第一列第i个或者不取第二列第j个。第一步转移方程就出来了:dp[i][j][p]=max(dp[i][j][p],dp[i][j-1][p],dp[i-1][j][p]).

同样的,对于第一列来说,可以用m=1的情况更新答案,对于第二列也是这样。

现在只用看i=j时,还可以矩阵整体转移,也就是当前情况下,可以把同一行2个数压成一个数,然后再进行m=1的方式(m=1的思想其实很重要,具有延伸性)。

注意按照我这样写需要把断点从0开始,因为可以一次性全部取完(也是符合条件的)。

参考代码

#include<cstdio>
using namespace std;
int n,m,k,sum[101][5],a[101][5];
int max1(int p,int q) { return p>q?p:q; }
int dp[101][20],dp1[101][101][20];
int main()
{
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%d",&a[i][j]);
			sum[i][j]+=sum[i-1][j]+a[i][j];//处理前缀和 
		}
	}
	if(m==1)
	{
		int maxn=-999999999;
		dp[1][1]=a[1][1];dp[1][0]=0;
		for(int i=2;i<=n;i++)
		{
			for(int j=0;j<i;j++)
			{
				dp[i][j]=dp[i-1][j];//当前不取,必不可少 
				for(int z=1;z<=k;z++)
				{
					dp[i][z]=max1(dp[i][z],dp[j][z-1]+sum[i][1]-sum[j][1]);
					if(z==k) maxn=max1(maxn,dp[i][z]);//全程统计答案 
				}
			}
		}
		printf("%d",maxn);
	}
	else
	{
		for(int z=1;z<=k;z++)
		{
			for(int i=1;i<=n;i++)
			{
				for(int j=1;j<=n;j++)
				{
					dp1[i][j][z]=max1(dp1[i-1][j][z],dp1[i][j-1][z]);//不取。继承前面 
					for(int p=0;p<i;p++)//对于左边的列 
					dp1[i][j][z]=max1(dp1[i][j][z],dp1[p][j][z-1]+sum[i][1]-sum[p][1]);
					for(int q=0;q<j;q++)//对于右边的列 
					dp1[i][j][z]=max1(dp1[i][j][z],dp1[i][q][z-1]+sum[j][2]-sum[q][2]);
					if(i==j)//可能以矩阵的形式转移 
					for(int y=0;y<i;y++)
					dp1[i][j][z]=max1(dp1[i][j][z],dp1[y][y][z-1]+sum[i][1]-sum[y][1]+sum[j][2]-sum[y][2]);
				}
			}
		}
		printf("%d",dp1[n][n][k]);
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值