倒水(water.cpp)
试题描述: 某一天,在 NH 学习 CS 的 ZR 习得新的 ACM 秘籍,惊奇的发现,每使用一次会额外
得到一个容量无限大的瓶子,并且初始时每个瓶子里有 1 升水。当他一口气使用了 N 次秘 籍后发现瓶子实在太多了,于是他决定保留不超过 K 个瓶子。规则是每次选择两个当前含 水量相同的瓶子进行合并,把一个瓶子的水全部倒进另一个瓶子,然后把空瓶砸碎。 显然在某些情况下 ZR 无法达到目标,比如 N=3,K=1。此时 ZR 会重新使用秘籍获得
一些新的瓶子(新瓶子容量无限,开始时有 1 升水),以达到目标。 现在 ZR想知道,除去一开始使用了N次秘籍,最少还需要使用多少次秘籍才能达到目
标?
输入格式:
一行两个正整数N,K(1<=N<=109,K<=1000)。
输出格式:
一个非负整数,表示最少需要买多少新瓶子。
输入样例:
3 1
输出样例:
1
数据规模:
对于 30%的数据,N<=3*105;
对于 100%的数据如题目。
题解
题目要求只能把水量相同的合并,于是可以贪心地把能合并的都合并,可以发现最终的水量是一些2的幂次,而这些数值的和也就是瓶子的数量N。那么最后剩下的瓶子个数就是N二进制中“1”的个数。如果“1”的个数小于等于K,那就满足要求了。如果大于K,假设N的二进制中最后一个“1”位置右数第K个,那么就添加2K-1个新瓶,直到使得N的二进制中“1”的个数减少到K个或以下。求某个十进制下的数字在二进制下有多少个“1”,可以不断减LowBit的方法来求。
假设x的二进制中最后一个“1”位置右数第y个LowBit(x) = 2y-1.
时间复杂度:O(K)
空间复杂度:O(1)
代码
#include<bits/stdc++.h>
#define F(i,a,b) for( int i=(a);i<=(b);i++ )
#define N 1001
#define M 10001
#define LL long long
#define oo 0x7fffffff
using namespace std;
LL read()
{
LL f=1,s=0;
char ch=getchar();
while( ch>'9' || ch<'0' ) { if( ch=='-' ) f=-1; ch=getchar(); }
while( ch<='9' && ch>='0' ) { s=( s<<1 )+( s<<3 )+ch-'0'; ch=getchar(); }
return f*s;
}
LL m,n,k,t;
LL tot,ans;
LL a[N],b[N];
int fa,cnt;
LL init()
{
b[0]=1;
F( i,1,oo )
{
b[i]=b[i-1]*2;
if( b[i]>=n )
break;
}
while( n>1 )
{
a[cnt]=n%2;
n/=2;
cnt++;
}
a[cnt]=1;
}
LL cul( int x )
{
a[x]+=1;
while( a[x]==2 )
{
a[x]=0;
x++;
a[x]++;
}
int mmax=max( x,cnt );
for( int i=mmax;i>=fa;i-- )
{
if( a[i] ) t+=b[i];
}
return t-m;
}
int main()
{
freopen( "water.in","r",stdin );
freopen( "water.out","w",stdout );
n=read();
k=read();
m=n;
init();
for( int i=cnt;i>=0;i-- )
{
if( a[i] )
{
tot++;
if( tot==k+1 )
{
ans=cul( fa );
break;
}
fa=i;
}
}
cout<<ans<<endl;
return 0;
}