感觉比较难的一题,即使做第三遍也感觉比较吃力。题目描述很清楚,英文理解无障碍。此题实质:n个物体中选m个。
最容易想到的当然是搜索,即DFS。从1到n依次进行扫描,判断是否被选中。每选够m个,就与前面的结果进行一次比较。因为Max( n ) = 200,Max( m ) = 20所以搜索次数大致有 ,这样的规模应该比较恐怖。因此,如果不优化,暴搜必挂无疑。如何剪枝?也就是哪些状态不用搜索,可以直接跳过。例如,我们要在20个人中选10个人。在以前的搜索中,已经取得了最优解S。当前已经选择了5个人,如果按照目前的情况无论如何也不可能取到比S更优的解,这时就可以直接放弃,没有必要继续搜索剩下的5人。当然,按照这种思路可以剪枝。但是,算法本省已确定的了它的复杂度是指数级,剪枝能修改的只是系数。因此,意义不大。
#include <iostream> #include <cmath> using namespace std; //***********************常量定义***************************** const int NUM = 205; const int INF_MIN = -999999999; const int INF_MAX = 999999999; //*********************自定义数据结构************************* //********************题目描述中的变量************************ int n, m; int dValue[NUM]; int pValue[NUM]; //**********************算法中的变量************************** bool tmpUsed[NUM]; bool used[NUM]; int sub = INF_MAX; int sum = INF_MIN; //***********************算法实现***************************** void DFS( int curNum, int curId, int sumD, int sumP ) { int x = abs(sumD-sumP); int y = sumD+sumP; if( curNum == m ) { if( x < sub || ( x == sub && y > sum ) ) { memcpy( used, tmpUsed, sizeof(used) ); sub = x; sum = y; } return; } //此处可剪枝 //if( ??? ) return; int i; for( i=curId; i<n; i++ ) { if( !tmpUsed[i] ) { tmpUsed[i] = true; DFS( curNum+1, curId+1, sumD+dValue[i], sumP+pValue[i] ); tmpUsed[i] = false; } } } //************************main函数**************************** int main() { freopen( "in.txt", "r", stdin ); int caseNum = 0; while( cin >> n >> m, !( n == 0 && m == 0 ) ) { for( int i=0; i<n; i++ ) { cin >> pValue[i] >> dValue[i]; } sub = INF_MAX; sum = INF_MIN; memset( tmpUsed, false, sizeof(tmpUsed) ); memset( used, false, sizeof(used) ); DFS( 0, 0, 0, 0 ); int ansD = 0; int ansP = 0; for( int j=0; j<n; j++ ) { if( used[j] ) { ansD += dValue[j]; ansP += pValue[j]; } } cout << "Jury #" << ++caseNum << endl; cout << "Best jury has value " << ansP << " for prosecution and value " << ansD << " for defence:" << endl; for( int k=0; k<n; k++ ) { if( used[k] ) cout << " " << k+1; } cout << endl; } return 0; }
从动态规划出发,继续考虑。现在要在n个人中选出m个人,设所有人的编号:1…n。如果编号为n的人为被选中,那么问题转化为:在剩下的n-1个人中选m个。如果编号为n的人被选中,那么问题转化为:在剩下的n-1个人中选m-1个。
那么,编号为n的人到底是选还是不选?就要比较这两种方案,看那个更优了。
即,DP的状态转换方程:
DP[m,n,step]=Best(DP[m-1,n-1,step+dn-pn],DP[m,n-1,step])
step=sum(d)-sum(p).
Best(a,b)=(|a.d-a.p|<|b.d-b.q|)or((|a.d-a.p|==|b.d-b.q|)&&(a.d+a.p>b.d+b.q))?a:b.
DP比递归的优势在于打表,如何打表避免重复计算?在程序中用到flag[25][205][805]。这里相当于枚举D(j) – P(j), 每一个值对应数组的一个元素。范围[-m*20,m*20]--->[0,m*20*2],故数组大小选805。Flag[m][n][v],标记n个中选m个,且D(j) – P(j) +400的值为V的状态是否已计算过。
Source Code Problem: 1015 User: 3109034010 Memory: 31696K Time: 266MS Language: C++ Result: Accepted Source Code #include <stdio.h> #include <memory.h> int Sum_d[25],Sum_p[25],jury_d[205],jury_p[205]; int Best_d[25][205][805],Best_p[25][205][805]; char flag[25][205][805],choose[25][205][805]; int ans_jury[205]; int abs(int num)//返回num的绝对值 { if(num<0) return -num; else return num; } int DP(int n,int m,int& dp_d,int& dp_p) { int yes_d,yes_p,no_d,no_p,tm_d=dp_d,tm_p=dp_p; int tm1,tm2; if(m==0) { return 0; }//已经选够人 if(m==n) { dp_d+=Sum_d[m]; dp_p+=Sum_p[m]; return 0; }//在a个人里选a个。。 if(flag[m][n][400+tm_d-tm_p]) { dp_d+=Best_d[m][n][400+tm_d-tm_p]; dp_p+=Best_p[m][n][400+tm_d-tm_p]; return 0; }//已经算过的状态。 flag[m][n][400+dp_d-dp_p]=1; no_d=dp_d; no_p=dp_p; DP(n-1,m,no_d,no_p);//选择n yes_d=dp_d+jury_d[n]; yes_p=dp_p+jury_p[n]; DP(n-1,m-1,yes_d,yes_p);//抛弃n tm1=abs(yes_d-yes_p)-abs(no_d-no_p); tm2=(yes_d+yes_p)-(no_d+no_p); if((tm1<0)||((tm1==0)&&(tm2>0))) { dp_d=yes_d; dp_p=yes_p; choose[m][n][400+tm_d-tm_p]=1; } else { dp_d=no_d; dp_p=no_p; choose[m][n][400+tm_d-tm_p]=0; }//取最优解 Best_d[m][n][400+tm_d-tm_p]=dp_d-tm_d; Best_p[m][n][400+tm_d-tm_p]=dp_p-tm_p;//记录状态 return 0; } int doSearch(int m,int n,int pos)//搜索培训团组合。 { int i; if(m==0) return 0; if(m==n) { for(i=1;i<=n;i++) ans_jury[i]++; return 0; } if(choose[m][n][400+pos]==1) { ans_jury[n]++; doSearch(m-1,n-1,pos+jury_d[n]-jury_p[n]); } else { doSearch(m,n-1,pos); } return 0; } int main() { int i,m,n,t=0; int ans_d=0,ans_p=0; while(scanf("%d%d",&n,&m),m+n>0) { t++; Sum_d[0]=0; Sum_p[0]=0; ans_d=0; ans_p=0; memset(flag,0,sizeof(flag)); memset(choose,0,sizeof(choose)); memset(ans_jury,0,sizeof(ans_jury)); for(i=1;i<=n;i++) { scanf("%d%d",&jury_d[i],&jury_p[i]); if(i<=m) { Sum_d[i]=Sum_d[i-1]+jury_d[i]; Sum_p[i]=Sum_p[i-1]+jury_p[i]; } } DP(n,m,ans_d,ans_p); doSearch(m,n,0); printf("Jury #%d\nBest jury has value %d for prosecution and value %d for defence:\n",t,ans_d,ans_p); for(i=1;i<=n;i++) { if(ans_jury[i]) printf(" %d",i); } printf("\n\n"); } return 0; }
网上还有另外一种解法,枚举D(j) – P(j)。要选m个人,使|D(j) – P(j)|最小。如果已选了m-1个人,再选1个人即可。选的这一个人,只要使m个人的|D(j) – P(j)|最小即可。
Source Code Problem: 1015 User: 3109034010 Memory: 268K Time: 32MS Language: C++ Result: Accepted Source Code #include <iostream> using namespace std; int d[201]; //存储所有人的d值 int p[201]; //存储所有人的p值 int result[21]; //存储最后选出的所有人的编号 int dp[21][801]; //dp[j][k]表示 选出j个人,辩控差为k时的最大辩控和 //辩控差的范围:【-400,400】,用下标表示【0,800】 int path[21][801]; //path[j][k]表示 第j个被选中的人的下标 int cs = 0; //记录测试用例的数目 int m, n; //检测 已选择了a个人,辩控差为b且辩控和最大的方案 是否包含编号为i的人 bool CheckIsInPath( int a, int b, int i ) { while( (a>0) && path[a][b] != i ) { b -= ( p[path[a][b]] - d[path[a][b]] ); a--; } return (a>0) ? true:false; } //快排qsort的比较函数 int comp( const void *arg1, const void *arg2 ) { int result = *(int*)arg1 - *(int*)arg2; return result; } //使用DP解决问题 void Solve() { while( scanf( "%d%d", &n, &m ), n || m ) //逗号表达式的值为最后一个表达式的值,即m,n都不为零 { cs++; int i; for( i=1; i<=n; i++ ) { scanf( "%d%d", p+i, d+i ); } memset( dp, -1, sizeof( dp ) ); //dp[j][k] == -1,表示该方案不存在,即j个人的辩控查不可能为k memset( path, 0, sizeof( path ) ); int original = m * 20; //枚举辩控差,original对应实际辩控差为0,即【-m*20,m*20】---【0,m*20*2】 dp[0][original] = 0; //0个人,辩控差为0,最大辩控和为0 for( int j=0; j<m; j++ ) //m个人,编号【0,m-1】 { for( int k=0; k<=2*original; k++ ) //枚举辩控差,【0,m*20*2】 { if( dp[j][k] >= 0 ) //如果方案可行 { for( i=1; i<=n; i++ ) //bottom to up,尝试增加一个人 { if( dp[j+1][ k + p[i] - d[i] ] < dp[j][k] + p[i] + d[i] )//更新所有可能的辩控差 { if( !CheckIsInPath( j, k, i ) ) { dp[j+1][ k + p[i] - d[i] ] = dp[j][k] + p[i] + d[i]; path[j+1][ k + p[i] - d[i] ] = i; } } } } } } for( i=0; dp[m][original+i]<0 && dp[m][original-i]<0; i++ ); //从原点开始向两边扫描 //可以保证辩控差绝对值最小 int temp = ( dp[m][original+i] > dp[m][original-i] ) ? i : -i; //绝对值相等时,取辩控和最大 int sumP = ( dp[m][original+temp] + temp )/2; //(和-差)/ 2 int sumD = ( dp[m][original+temp] - temp )/2; //(和+差)/ 2 printf( "Jury #%d\n", cs ); printf( "Best jury has value %d for prosecution and value %d for defence:\n", sumP, sumD ); memset( result, 0, sizeof(result) ); int s = original+temp; for( i=1; i<=m; i++ ) { result[i] = path[m+1-i][s]; s -= p[result[i]] - d[result[i]]; } qsort( result+1,m,sizeof(int), comp ); for( i=1; i<=m; i++ ) printf( " %d", result[i] ); printf( "\n\n" ); } } int main() { Solve(); return 0; }
参考文档:
http://blog.163.com/szuice_cream/blog/static/93813254200881171731604/
http://www.cppblog.com/mythit/archive/2010/10/14/88378.html?opt=admin