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