poj 1015 Jury Compromise
枫林晚
2018-04-11 16:58:25
# 震惊!李煜东、POJ等99%的人都错了!
## 快来DEBUG!
### 题目大意:
在遥远的国家佛罗布尼亚,嫌犯是否有罪,须由陪审团决定。陪审团是由法官从公众中挑选的。先随机挑选n 个人作为陪审团的候选人,然后再从这n 个人中选m 人组成陪审团。选m 人的办法是:控方和辩方会根据对候选人的喜欢程度,给所有候选人打分,分值从0 到20。为了公平起见,法官选出陪审团的原则是:选出的m 个人,必须满足辩方总分D和控方总分P的差的绝对值|D-P|最小。如果有多种选择方案的 |D-P| 值相同,那么选辩控双方总分之和D+P最大的方案即可。
输出:
选取符合条件的最优m个候选人后,要求输出这m个人的辩方总值D和控方总值P,并升序输出他们的编号。
——https://blog.csdn.net/lyy289065406/article/details/6671105 (然而这个“标程”是有BUG的)
### 前言:
这个题,网上流传的绝大多数都是错的解法,之所以能流传,因为poj上的数据输出也是错误的。导致真正正确的程序因为WA不止而被埋没。
李煜东书上的思路没错,但是转移的实现是有问题的。然而书后附加的光盘上的“标程”,却是李煜东本人从别人那里扒的。所以不但与李煜东本人思路不太符合,而且错误。
随后在discuss里终于出现了CZDleaf等神犇,找出了bug并且出了hack数据并且给出了真正的标程。
https://blog.csdn.net/glqac/article/details/22687243
http://poj.org/showmessage?message_id=161937
### 分析:
考虑到每个候选人只有选或者不选两种情况,而且之前不是最优解的人可能之后也要被选上。所以做法是0/1背包。
设f[j][k]表示选了j个人,差值为k的D+P的最大值。
状态转移方程:f[j][k]=max(f[j][k],f[j-1][k-(p[i]-d[i])]+p[i]+d[i])
最初f[0][0]=0,其余为-1或者-0x3f3f3f3f
由于可能会使下标变成负数,所以增加一个修正值fix=20*m
(因为差值最多为20*m,令fix映射0,使映射区间向右平移fix个单位长度)
### 错误方法:
外层循环j:1~m;
中层循环k: 0~2*fix;
内层循环i:1~n
每次尝试更新的时候,检查一下之前路径中是否已经有了i,没用过就继续更新,否则conitnue.
这样可以避免路径改变的问题,每一次更新,令pre[j][k]=i即可。最后的时候,从f[m][fix]向两边找到第一个值不为-1的k即为差值。
(此处省略若干字)
错误的原因:
bug之处在于:如果在选择j之前,选择1~j-1的f[j-1][k-(p[i][-d[i])最优方案不止一个,即使得f[j-1]最大化的路径不止有一条,那么可能的情况是,编号较小的路径组合会覆盖、掩盖之后的路径组合(它只能记录一条),而正确的答案却是从后面的路径转移过来的。换句话说,我们在更新的时候,有可能抛弃了正确答案的转移路径。从而选择j时的正解可能会因为之前选过而被pass掉。
无法保证最优子结构条件。
### 正解:
外层循环i:1~n
中层**倒序循环**j:m~1;
内层循环k:0~2*fix;
这样,因为避免了重复选择判断的一项,而且由于i的顺序循环,前面即使出现重复的路径,也不会对之后的答案造成影响,而且避免了最后的sort麻烦。
至于路径转移:(被卡了)博客上给的是用vector直接复制之前的路径进行转移。这样,即使f[1~m-1][k]的路径变化了,也不会影响到决策的输出。(f[m][k]的路径是孤立的存在,不用递推往前找,从而避免了麻烦。)
正解代码:
```cpp
#include<cstdio>
#include<ctype.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
int dp[21][801];
vector<int> path[21][801];
int main()
{
int times=1;
// freopen("input.txt","r",stdin);
// freopen("output.txt","w",stdout);
int subtraction[201],_plus[201];
int n,m,i,j,k;
while(~scanf("%d%d",&n,&m) && n && m)
{
for(i=0;i<m;++i)//清空vector
for(j=0;j<801;++j)
path[i][j].clear();
memset(dp,-1,sizeof(dp));
int d,p;
for(i = 0; i < n; i++)
{
cin>>d>>p;
subtraction[i] = d-p;
_plus[i] = d+p;
}
int fix = 20*m;
dp[0][fix] = 0;
for(k = 0; k < n; k++)//选择一个
for(i = m-1; i >= 0; i--)//进行逆推
{
for(j = 0; j < 2*fix; j++)
{
if(dp[i][j] >= 0)
{
if(dp[i+1][j+subtraction[k]] <= dp[i][j] + _plus[k])
{
dp[i+1][j+subtraction[k]] = dp[i][j] + _plus[k];
path[i+1][j+subtraction[k]] = path[i][j];//每次更新都要把path全部复制过来,就是因为这个才用的vector
path[i+1][j+subtraction[k]].push_back(k);
}
}
}
}
for(i = 0; dp[m][fix+i] == -1 && dp[m][fix-i] == -1; i++);
int temp = (dp[m][fix+i] > dp[m][fix-i]) ? i : -i;
int sumD = ( dp[m][fix+temp] + temp )/2;
int sumP = ( dp[m][fix+temp] - temp )/2;
printf( "Jury #%d\n", times++ );
printf( "Best jury has value %d for prosecution and value %d for defence:\n", sumD,sumP);
for( i=0; i < m; i++ )
printf( " %d", path[m][fix+temp][i]+1);
printf( "\n\n" );
}
return 0;
}
```
总结:
1.循环的顺序对于背包问题极为重要。
2.路径转移的方法需要再积累。
3.关于路径覆盖的问题,无后效性,最优子结构的理解还要加深。