给n件东西,第i件的重量为1 / 2^ki,问能否分成两堆,使每一堆的重量都>=0.5,如果能,输出YES并在第二行输出分配方案。如果不能,输出NO
1 <= T <= 2000
1 <= n <= 1e5
n总和 <= 7e5
1 <= ki <= 1e9
3
3
2 2 2
3 2 2 1
2
1 1
Case 1: NO
Case 2: YES
001
Case 3: YES
10
赛前做了蔡队出的牛客小白赛的题:https://www.nowcoder.com/acm/contest/190/G
队友说,近似重量地分成两堆的情况是最贪心的,这样说的话,感觉JUSTICE这道题跟蔡队出的这个题还是蛮类似的。于是开始回忆自己的做法。。突然想起蔡队出的小白赛数据规模很小,水过去的。代码如下:
#include
using namespace std;
typedef long long ll;
const int maxn = 100010;
const double eps = 1e-8;
const int INF = 0x3f3f3f3f;
set<int>s;
vector<int>v;
void redirect(){
#ifdef LOCAL
freopen("test.txt","r",stdin);
#endif
}
int main(){
redirect();
int n,x,T,sum = 0;
scanf("%d",&n);
for(int i = 0;i < n;i++){
scanf("%d",&x);
sum += x;
v.clear();
for(auto it = s.begin();it != s.end();it++)
v.push_back(*it + x);
for(auto it = v.begin();it != v.end();it++)
s.insert(*it);
s.insert(x);
}
int mid = sum/2,p = 0;
while((x = mid-p++) >= 0){
if(s.count(x)){
printf("%d %d\n",x,sum-x);
return 0;
}
}
return 0;
}
赛时还对这个代码进行了重现,几乎写完的时候才意识到,这个写法就算时间空间过得去,也不能记录选择方案。遂舍弃(打铁可真难受
下面才是正确 的做法(可不一定,毕竟我没法去judge一个赛后写出来的代码的正确性)
结论:当且仅当总质量小于1时无解,当总质量不小于1时,总能选出一个集合,其总质量为1/2.
证明:
令x + 1/2^k = y (x < 1/2 <= y)
1/2^k | x 并且1/2^k | 1/2
从而1/2^k | (1/2 - x) <= (y-x) = 1/2^k
y = 1/2
到了这里还面临一个卡精度的问题,因为1<<1e5是算不出来的,同样1/2^1e5也是算不出来的,如果有以下这种BT样例:
1
100000
1 2 18 18 18……18(1e5-2个18)
总值 = 1/2 + 1/4 + (1e5-2)/2^18 > 1
而舍弃1/2^1e5时计算出来的结果时 1/2 + 1/4 < 1
所以我就杜撰出来一个1/2^k的分数加法:
举个栗子:
1/2^2 + 1/2^3 = 2^0/2^2 + 2^0/2^3 = (2^(3-2) + 2^0)/2^3
可以发现分子是一系列2^x的和,我们可以用set之类结构存起来这些x
假如某次通分会产生两个相同的2^x,采取的策略是消去x,添加一个x+1
这个过程重复执行,直到不存在相同的x
分母是一个2^y,为了不计算出来具体值,只记录分母的y值。
可以发现,假如y-max(xi) <= 1,则代表这个分数>= 0.5,如果x>=y,则代表这个数>=1
代码实现:
#include
using namespace std;
typedef long long ll;
static const int maxn = 100010;
static const double eps = 1e-8;
static const int INF = 0x3f3f3f3f;
static const double pi = acos(-1);
void redirect(){
#ifdef LOCAL
freopen("test.txt","r",stdin);
#endif
}
bool flag;//判断sum是否已经过了1/2
int ans[100010];//记录分配方案
struct node{//分数,son是分子,mom是分母
set<int>son;
int mom;
void clean(){
son.clear();
mom = 0;
}
}p;
struct Num{//为了分数从大到小相加,需要有一个排序,为了id和数对得上,搞一个结构体。
int id,val;
bool operator <(const Num& b)const{
return this->val < b.val;
}
}num[100010];
inline int get_max(){//获取分子中2的最高次项的次数
auto it = p.son.end();
it--;
return *it;
}
inline void add(const int& s){//现有分数p+一个新的分数
if(p.mom < num[s].val){//现有分数p的分子比新加的分数的分子小,现有分数p的分子多项式要乘以一个2^tmp,每一项次数+tmp,再加一个0
int tmp = num[s].val - p.mom;
p.mom = num[s].val;
vector<int>v;//临时存储用
for(auto it = p.son.begin();it != p.son.end();it++)
v.push_back(*it + tmp);
p.son.clear();
p.son.insert(0);
for(auto it = v.begin();it != v.end();it++)
p.son.insert(*it);
}
else{//现有分数的分子大,很舒服,新加的分数分子2^0乘一个2^tmp加到分子多项式中
int tmp = p.mom - num[s].val;
while(p.son.count(tmp))p.son.erase(tmp++);
p.son.insert(tmp);
}
if(flag == false){//sum过半则不再标记ans
ans[num[s].id] = 1;
if(p.mom - get_max() <= 1)flag = true;
}
}
int main(){
redirect();
int n,T,x;
scanf("%d",&T);
for(int t = 1;t <= T;t++){
memset(ans,0,sizeof(ans));
flag = false;
p.clean();
printf("Case %d: ",t);
scanf("%d",&n);
for(int i = 1;i <= n;i++){
scanf("%d",&x);
num[i].id = i;
num[i].val = x;
}
sort(num+1,num+1+n);
p.mom = num[1].val;
p.son.insert(0);
ans[num[1].id] = 1;
if(p.mom - get_max() <= 1)goto label;//用goto队友别骂我
for(int i = 2;i <= n;i++)add(i);
if(get_max() < p.mom){
puts("NO");
continue;
}
label:
puts("YES");
for(int i = 1;i <= n;i++)printf("%d",ans[i]);
puts("");
}
return 0;
}
总之,这回现场赛CDEI题真让人自闭了,天灾人祸+flag飞起,说好的不打铁呢?结果止步于39’两道签到题A,B???唉。。还是太菜了呀。。