第一题 Jury Compromise http://poj.org/problem?id=1015
大致题意是有n对数{ai,bi}里面选择m对使得这m对的|ai-bi|最小并最大化ai+bi。
看起来很简单其实一点都不简单。刚开始想的时候一直在纠结,如果每个子状态表示的是使得|ai-bi|最小的状态那么状态没有办法转移,因为前面局部最小和全局最小没有什么关系。然后搜了一下解析:参考
总结一下本题有这么几个关键点:
预处理,设ci=ai-bi,di=ai+bi,这是很自然的。接下来问题转换为ci的和绝对值最小而di最大。
1.状态的设计。我觉得这个是最关键的一点,有了正确的状态设计,下面的步骤其实都可以想到了。首先数据范围很小,所以状态应该记录比最小更多的信息,想到把每一种可能的和都记下来,但是这样就失去了dp的意义。所以就记录i个数满足c的和为j的最大的d的和,这样就记录了足够多的信息来转移了。还有就是dp=-1表示这种状态达不到。也容易发现这种状态的定义是没有后效性的,因为d>0所以i个的最大一定是由某个i-1的最大转移过来的。
2.转移的方法。转移的方法其实也挺难想的。由于dp[i][j]表示i个数c和为j且d和最大的状态,那么该状态一定是由某一个dp[i-1][x]和某个数t转移过来的,需要满足的条件是x+ct=j,t不在dp[i-1][j]的状态中,dp[i-1][x]+dt最大。由上面的分析,可以考虑
3.记录路径的方法。用path[i][j]记录方案dp[i][j]最后一个人的编号,这样就可以顺藤摸瓜寻找到来时的路径了。
4.平移下标使数组不要溢出。数组的下标只能为正数而数字可能为负,所以需要整体平移一下才能装进数组里。
#include <iostream>
#include <cmath>
#include <string.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#define INF 0x7fffffff
#define REP(i,a,b) for(int i=(a);i<=(b);++i)
#define PER(i,a,b) for(int i=(a);i>=(b);--i)
using namespace std;
int n; //候选人数
int m; //当选人数
int dp[21][801]; //dp[j][k]:取j个候选人,使其辩控差为k的所有方案中,辩控和最大的方案的辩控和
int path[21][801]; //记录所选定的候选人的编号
int a[210],b[210],c[210],d[210];
//回溯,确认dp[j][k]方案是否曾选择过候选人i
bool select(int j,int k,int i,int* c){
while(j>0 && path[j][k]!=i){
k-=c[ path[j][k] ];
j--;
}
return j?false:true;
}
int main()
{
int time=1;
while(cin>>n>>m)
{
if(m==0&&n==0)break;
memset(a,0,sizeof(a));memset(b,0,sizeof(b));
memset(c,0,sizeof(c));memset(d,0,sizeof(d));
memset(dp,-1,sizeof(dp));
memset(path,0,sizeof(path));
REP(i,1,n){
cin>>a[i]>>b[i];
c[i]=a[i]-b[i];
d[i]=a[i]+b[i];
}
int fix=m*20; //总修正值,修正极限为从[-400,400]映射到[0,800]
dp[0][fix]=0; //由于修正了数值,因此dp[0][fix]才是真正的dp[0][0]
REP(j,1,m)REP(k,0,2*fix){
if(dp[j-1][k]>=0){ //区间已平移,dp[0][fix]才是真正的dp[0][0]
REP(i,1,n)if(dp[j][ k+c[i] ] < dp[j-1][k]+d[i]){
if(select(j-1,k,i,c)){
dp[j][ k+c[i] ] = dp[j-1][k]+d[i];
path[j][ k+c[i] ] = i;
}
}
}
}
int k=0;
REP(i,0,fix)if(dp[m][fix-i]>=0 || dp[m][fix+i]>=0){k=i;break;}//从中间向两边搜索最小辨控差的位置i
int div=0;
if(dp[m][fix-k] > dp[m][fix+k]) div=(fix-k);
else div=(fix+k); //最小辨控差
cout<<"Jury #"<<time++<<endl;
cout<<"Best jury has value ";
//辩方总值 = (辨控和+辨控差+修正值)/2
cout<<(dp[m][div]+div-fix)/2<<" for prosecution and value ";
//控方总值 = (辨控和-辨控差+修正值)/2
cout<<(dp[m][div]-div+fix)/2<<" for defence:"<<endl;
int id[30]={0};
for(int i=0,j=m,k=div;i<m;i++){
id[i]=path[j][k];
k-=c[ id[i] ];
j--;
}
sort(id,id+m); //升序输出候选人编号
REP(i,0,m-1)
cout<<' '<<id[i];
cout<<endl<<endl;
}
return 0;
}
第二题 炮兵阵地 POJ 1185
大致题意是有一个100*10的01矩阵,可以在0的地方放东西,但是放完之后它的上下左右两格以内都不能再放,问最多能放多少个东西。
这题是典型的状态压缩DP,但是有一点难想的地方就是怎么表示状态才能转移。最后上网搜了一下答案,发现是用dp[i][j][k]表示第i行,第j种可能状态,从第i-1行的第k种状态转移过来。想想也对,当某一种状态只和上一个状态有关的时候那么只需要表示这一种状态就可以了,但是如果这种状态和上一个以及上上个状态都相关的话就要开三维数组表示这一个和上一个的状态才能转移,也就是说要多开一维。
状态转移的话就是dp[i][j][k]=max(dp[i][j][k],dp[i-1][k][t]+num[j]);发现状压dp其实是把每一种两行里面可能的状态都搜过的,只不过把之前的不会受到影响的状态都压在了一起,所以时间就快了。
我还犯了几个错。初始化没有搞清楚,把第一行的状态混在整个循环里面导致凭空多了一层循环,于是欢快的TLE了;还有就是最大的状态不一定是最后一行要放东西而可能是在任何可能的位置,所以判断最后的答案的时候每一种状态都应该看一下;最后一个盯了一个小时才看出来的bug输入的时候行和列写放了mmp。
#include <iostream>
#include <cmath>
#include <string.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#define INF 0x7fffffff
#define REP(i,a,b) for(int i=(a);i<=(b);++i)
#define PER(i,a,b) for(int i=(a);i>=(b);--i)
using namespace std;
int m,n;
int dp[110][60][60];
int map[110];//地形1是山0是平地
int state[60];//不考虑地形的合法状态
int num[60];//每一种合法状态的1的个数
int prep(){
int cnt=0;
REP(i,0,(1<<m)-1){
if(!(i&(i<<1))&&!(i&(i<<2))){
state[cnt]=i;
int count=0;
REP(j,0,m-1){
if(i&(1<<j))count++;
}
num[cnt]=count;//cout<<"test"<<count<<endl;
cnt++;
}
}
return cnt;
}
int judge2(int x,int y){
if(x&y)return 0;return 1;
}
int judge(int k,int x){
if(map[k]&state[x])return 0;return 1;
}
string s[110];
int main(){
while(cin>>n>>m){
REP(i,0,n-1){
cin>>s[i];
REP(j,0,m-1){
if(s[i][j]=='H')map[i]+=(1<<(j));
}
}
//REP(i,0,n-1)cout<<"map"<<map[i]<<endl;
int ste=prep();//状态总数
//cout<<ste<<endl;
memset(dp,-1,sizeof(dp));
int ans=-1;
REP(st0,0,ste-1){
if(judge(0,st0)){
dp[0][st0][0]=num[st0];//cout<<num[st0]<<' '<<st0<<endl;
}
}
REP(i,1,n-1){//循环行数
REP(sti,0,ste-1){
if(!judge(i,sti))continue;
//cout<<i<<' '<<sti<<endl;
REP(sti_1,0,ste-1){
if((state[sti]&state[sti_1]))continue;
REP(sti_2,0,ste-1){
if(state[sti]&state[sti_2])continue;
if(dp[i-1][sti_1][sti_2]==-1)continue;
dp[i][sti][sti_1]=max(dp[i][sti][sti_1],dp[i-1][sti_1][sti_2]+num[sti]);
//cout<<i<<' '<<sti<<' '<<sti_1<<' '<<dp[i][sti][sti_1]<<endl;
}
}
}
}
REP(i,0,n-1)REP(j,0,ste-1)REP(k,0,ste-1){
//cout<<i<<' '<<j<<' '<<k<<' '<<dp[i][j][k]<<endl;
if(dp[i][j][k]>ans)ans=dp[i][j][k];
}
cout<<ans<<endl;
}
return 0;
}
第四题 To The Max POJ1050
题目大意是求一个矩阵的元素和最大的子矩阵。
虽然暴力水过了但是还是要知道一下O(n^3)的算法的,首先将第i行到第j行的矩阵压成一维然后求这个一维矩阵的最大子段和。dp[k]表示以第k个元素结尾的和最大的子段,然后如果之前的dp[k-1]<0,那么dp[k]=num[k],如果大于0那么dp[k]=dp[k-1]+num[k]。最后扫一下最大的dp[k]即可。代码就不贴了。