二分答案(最大化平均值)
POJ3111
有n个物品的重量与价值分别为
w
i
w_i
wi和
v
i
v_i
vi。从中选出k个物品使得单位价值重量最大,并输出序号。
利用二分搜索法,条件C(x):选择使得单位重量的价值不小于x,原问题转化为求满足C(x)最大的x。
在物品集合S中,他们单位重量的价值为
S
=
∑
i
=
1
k
v
i
∑
i
=
1
k
w
i
S=\frac{\sum_{i=1}^kv_i}{\sum_{i=1}^kw_i}
S=∑i=1kwi∑i=1kvi
判断S是否满足
∑
i
=
1
k
v
i
∑
i
=
1
k
w
i
≥
x
\frac{\sum_{i=1}^kv_i}{\sum_{i=1}^kw_i} \ge x
∑i=1kwi∑i=1kvi≥x
将不等式变形得到
∑
i
=
1
k
(
v
i
−
x
×
w
i
)
≥
0
\sum_{i=1}^k (v_i-x\times w_i)\ge 0
i=1∑k(vi−x×wi)≥0
对(
v
i
−
x
×
w
i
v_i-x\times w_i
vi−x×wi)的值进行排序贪心的选取
C(x)=(
v
i
−
x
×
w
i
v_i-x\times w_i
vi−x×wi)从大到小排列中的前k个的和不小于0
每次判断复杂度是O(nlogn)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct nice{
int v,w,xh;//序号
double y; //v-x*w
}E[100020];
int n,k;
bool cmp(nice a,nice b)
{
return a.y>b.y;
}
bool C(double x)
{
for(int i=1;i<=n;i++)
E[i].y=E[i].v-x*E[i].w;
sort(E+1,E+n+1,cmp);
double sum=0;
for(int i=1;i<=k;i++)
sum+=E[i].y;
return sum>=0;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){scanf("%d%d",&E[i].v,&E[i].w);E[i].xh=i;}
double lb=0,ub=1000005;
for(int i=1;i<100;i++)
{
double mid=(lb+ub)/2;
if(C(mid))lb=mid;
else ub=mid;
}
for(int i=1;i<=k;i++)
printf("%d ",E[i].xh);
//printf("%.2f",ub);
return 0;
}
反转(开关问题)
POJ3276
N头牛排成一列,牛头向前或向后。拥有一台自动转向机器,设定数值K,每次使用可以令K头连续的牛转向。求为了让所有的牛都面向前方需要的最少操作次数M和对应最小的K。
首先交换区间顺序对结果是没有影响的,此外对同一个区间进行两次以上反转是多余的。因此,问题转化成了求需要被反转的区间的集合。于是先考虑最左端的牛,包含这头牛的区间只有一个,若这头牛面朝后方,对应的区间就必须反转,此后这个最左的区间也不用再考虑了。这样通过首先考虑最左端的牛,问题的规模缩小了1,不断重复就可以无需搜索求出最少所需反转次数。
维护区间反转的部分复杂度降为(
N
2
N^2
N2)M[i]:区间[i,i+k-1]进行反转则为1,否则为0.
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int F[6000];
int M[6000];//记录区间[i,i+k-1]是否反转
int n;
int calc(int k)
{
memset(M,0,sizeof(M));
int res=0;//总反转次数
int sum=0;//前面片段的反转次数
for(int i=1;i+k-1<=n;i++)
{
if((F[i]+sum)%2==1)
{
res++;
M[i]=1;
}
sum+=M[i];
if(i-k>=0)sum-=M[i-k+1];
}
for(int i=n-k+2;i<=n;i++)
{
if((F[i]+sum)%2==1)return -1;
if(i-k>=0)sum-=M[i-k+1];
}
return res;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
char x;
cin>>x;
if(x=='B')F[i]=1;
if(x=='F')F[i]=0;
}
int K=1,M=n;
for(int k=1;k<=n;k++)
{
int m=calc(k);
if(m>=0 && m<M){
M=m;K=k;
}
}
printf("%d %d",K,M);
}
POJ3279
有N
×
\times
×M个格子,每格可以反转正反面,一面黑一面白,最终要使所有格子都反转成白色,每次反转时与它上下左右邻接的格子也会反转,求最少翻转次数,最小步数多个解时,输出字典序最小一组,解不存在输出IMPOSSIBLE.
首先同一个格子翻转多次是多余的。先看看左上角的格子,在这里除了反转(1,1)外,反转(1,2)和(2,1)也可以将这个格子反转,所以像上题直接确定的方法行不通。
于是不妨先指定好最上面一行的反转方法,此时能反转(1,1)的就只剩(2,1)了,所以只需判断(2,1)是否需要反转,如此反复下去就可以确定所有各自的判断方法。最后(N,1)~(N,M)如果并非全为白色,就意味着当前不存在可行的操作方法。
这个算法最上面一行的反转方式共有
2
N
2^N
2N种,复杂度为O(MN
2
N
2^N
2N).
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int M[18][18];//记录原始情况
int F[18][18];//记录反转情况
int A[18][18];//记录最优情况
const int dx[5]={-1,0,0,0,1};
const int dy[5]={0,-1,0,1,0};
int n,m;
int get(int x,int y)
{
int a=M[x][y];
//a+=(F[x-1][y]+F[x][y-1]+F[x][y]+F[x][y+1]);
for(int d=0;d<5;d++)
{
int x1=x+dx[d],y1=y+dy[d];
if(1<=x1 && x1<=n && 1<=y1 && y1<=m)a+=F[x1][y1];
}
return a%2;
}
int calc()
{
for(int i=2;i<=n;i++)
for(int j=1;j<=m;j++)
if(get(i-1,j)==1)F[i][j]=1;
for(int i=1;i<=m;i++)
if(get(n,i)==1)return -1;
int res=0;//记录反转次数
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(F[i][j])res++;
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&M[i][j]);
int ans=-1;
for(int k=0;k<1<<m;k++)
{
memset(F,0,sizeof(F));
for(int i=m;i>=1;i--)F[1][i]=k>>(i-1)&1;
int y=calc();
if(y>=0 && (ans<0 || ans>y))
{
ans=y;
memcpy(A,F,sizeof(F));
}
}
if(ans<0)printf("IMPOSSIBLE\n");
else{
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
printf("%d%c",A[i][j],j==m?'\n':' ');
}
}
弹性碰撞
POJ3684
在H米高处,存在一个垂直管将N个半径R厘米的球一一放入其中,实验开始时第一个球由于重力释放并掉落。此后将球每秒释放一次直到所有球都释放完毕。假设球与球或地面间的碰撞是弹性碰撞。问实验开始后T秒钟时每个球低端的高度。假设重力加速度为g=10m/
s
2
s^2
s2。
只有一个球时,它从高为H的位置落下花费时间为
t
=
2
H
g
t=\sqrt{\frac {2H}g}
t=g2H在时刻T时,令k为满足kt
⩽
\leqslant
⩽T最大整数,那么
y
=
H
−
1
2
g
(
T
−
k
t
)
2
(
k
是
偶
数
时
)
y=H-\frac 12g{(T-kt)}^2(k是偶数时)
y=H−21g(T−kt)2(k是偶数时)
y
=
H
−
1
2
g
(
k
t
+
t
−
T
)
2
(
k
是
奇
数
时
)
y=H-\frac 12g{(kt+t-T)}^2(k是奇数时)
y=H−21g(kt+t−T)2(k是奇数时)
当两球碰撞时将它们看作互相穿过继续运动,忽略碰撞,将计算得到的坐标进行排序后,就能知道每个球的最终位置。
因为半径R>0,当第i个球落入时,由于下面已经存在(i-1)个球,相当于底面被抬高了2R(i-1)cm,在R=0的结果上加上抬高值即可。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const double g=10.0;
int N,H,R,T;
double y[10020];//球的最终位置
double calc(int T1)
{
if(T1<0)return H;
double t=sqrt(2*H/g);
int k=(int)(T1/t);
if(k%2==0){
double d=T1-k*t;
return H-g*d*d/2;
}
else{
double d=k*t+t-T1;
return H-g*d*d/2;
}
}
int main()
{
int t;
cin>>t;
while(t--)
{
scanf("%d%d%d%d",&N,&H,&R,&T);
for(int i=1;i<=N;i++)
y[i]=calc(T-i+1);
sort(y+1,y+N+1);
for(int i=1;i<=N;i++)
printf("%.2f%c",y[i]+2*R*(i-1)/100.0,i==N?'\n':' ');
}
}
折半枚举
poj2785
各有n个整数的四个数列,A,B,C,D。从每个数列各取出一个数,使四个数的和为0。求组合个数,当一个数列有多个相同数字时,将他们作为不同的数字看待。
限制条件
⋅
1
≤
n
≤
4000
\cdot1\le n\le4000
⋅1≤n≤4000
⋅
∣
数
字
的
值
∣
≤
2
28
\cdot|数字的值|\le2^{28}
⋅∣数字的值∣≤228
从四个数列选择共有
n
4
n^4
n4种情况,全都判断一边不可行。不过将它们对半分成AB和CD再考虑,就可以快速解决了。从2个数列中选择的话只有
n
2
n^2
n2种组合,所以可以进行枚举。先从C,D中取出c,d,为了使总和为零则需要从A,B中取出a+b=-c-d。因此先将A,B中取数字的
n
2
n^2
n2种方法全枚举出来,排好序后利用二分搜索,算法复杂度是O(
n
2
log
n
n^2\log n
n2logn)。
有时,问题规模较大,无法枚举全部元素的组合,但能够枚举一半元素的组合。此时,将问题拆成两本分别枚举,再合并它们的结果这一方法往往非常有效。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=4200;
int A[maxn],B[maxn],C[maxn],D[maxn];
int AB[maxn*maxn];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d%d%d%d",&A[i],&B[i],&C[i],&D[i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
AB[(i-1)*n+j]=A[i]+B[j];
sort(AB+1,AB+n*n+1);
long long ans=0;
//
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
int cd=-C[i]-D[j];
ans+=upper_bound(AB+1,AB+1+n*n,cd)-lower_bound(AB+1,AB+1+n*n,cd);
}
//
printf("%lld",ans);
return 0;
}
因为觉得用不惯lower_bound和upper_bound,自己又打了个二分(给自己增加工作量,裂开)
先排除超出限制的-c-d,然后二分后的结果一定是AB[lb]
≤
\le
≤cd
≤
\le
≤AB[ub],然后再根据左右的大小具体分析。
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
int cd=-C[i]-D[j];
if(cd<AB[1]|| cd>AB[n*n])continue;
int l,r;
int lb=1,ub=n*n;
for(int i=1;i<30;i++)
{
int mid=(lb+ub)/2;
if(AB[mid]<cd)lb=mid;
else ub=mid;
}
if(AB[lb]!=cd && AB[ub]!=cd)continue;
if(AB[lb]==cd)l=lb;
else l=ub;
lb=1,ub=n*n;
for(int i=1;i<30;i++)
{
int mid=(lb+ub)/2;
if(AB[mid]<=cd)lb=mid;
else ub=mid;
}
if(AB[ub]==cd)r=ub;
else r=lb;
r++;
ans+=r-l;
}
超大背包问题
有重量和价值分别
w
i
w_i
wi,
v
i
v_i
vi的n个物品。从这些物品挑选总重量不超过W的物品,求所有挑选的方案中价值总和的最大值。
限制条件
⋅
1
≤
n
≤
40
\cdot1\le n\le40
⋅1≤n≤40
⋅
1
≤
w
i
,
v
i
≤
1
0
15
\cdot1\le w_i,v_i\le10^{15}
⋅1≤wi,vi≤1015
⋅
1
≤
W
≤
1
0
15
\cdot1\le W\le10^{15}
⋅1≤W≤1015
使用DP求解背包问题的复杂度是O(nW),因此不能用来解决这里的问题,此时应用n比较小的特点来寻找其他办法。
挑选物品的的方法共有
2
n
2^n
2n所以不能直接枚举,单想前面拆成两半后再枚举的话,因为每部分只有20所以是可行的。先把前半部分的选取方法对应的重量和价值记为w1,v1,然后在后半部分寻找总重w2
≤
\le
≤W-w1时时v2最大的选取方法就好了。
因此需要思考如何从枚举得到的(w2,v2)的集合中高效寻找max{v2|w2
≤
\le
≤W}。首先能够排除所有w2[i]
≤
\le
≤w2[j]并且v2[i]
≥
\ge
≥v2[j]的j,这点按照w2,v2的字典序排序后简单做到。此后剩余的元素都满足w2[i]
≤
\le
≤w2[j] && v2[i]
≤
\le
≤v2[j],要计算max{v2|w2
≤
\le
≤W}的话,只需要寻找满足w2[i]
≤
\le
≤W的最大的i就可以了,可以用二分搜索完成。剩余元素个数为M的话,一次搜索需要O(
log
\log
logM)的时间。因为M
≤
2
(
n
2
)
\le2^{(\frac n2)}
≤2(2n),所以这个算法总的复杂度是O(
2
(
n
2
)
n
2^{(\frac n2)}n
2(2n)n),可以在时限内解决这个问题。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=60;
struct nice{
long long w1,v1;
}E[maxn];
long long w[maxn],v[maxn];
bool cmp(nice a,nice b)
{
return a.w1<b.w1;
}
int main()
{
int n,W;
scanf("%d%d",&n,&W);
for(int i=1;i<=n;i++)
scanf("%lld",&w[i]);
for(int i=1;i<=n;i++)
scanf("%lld",&v[i]);
//枚举前半部分
int n2=n/2;
for(int i=0;i<1<<n2;i++)
{
long long sw=0,sv=0;
for(int j=1;j<=n2;j++)
if(i>>(j-1)&1)sw+=w[j],sv+=v[j];
E[i+1].w1=sw,E[i+1].v1=sv;
}
//去除多余元素
sort(E+1,E+(1<<n2)+1,cmp);
int m=2;
for(int i=2;i<=1<<n2;i++)
if(E[m-1].v1<E[i].v1)E[m++]=E[i];
for(int i=1;i<=m-1;i++)
printf("%d %d\n",E[i].w1,E[i].v1);
//枚举后半部分求解
long long res=0;
for(int i=0;i<1<<(n-n2);i++)
{
long long sw=0,sv=0;
for(int j=1;j<=(n-n2);j++)
if(i>>(j-1)&1)sw+=w[n2+j],sv+=v[n2+j];
if(sw<=W)
{
int lb=1,ub=m-1;
for(int i=1;i<5;i++)
{
int mid=(lb+ub)/2;
if(E[mid].w1<=(W-sw))lb=mid;
else ub=mid;
}
if(E[ub].w1>(W-sw))ub=lb;
res=max(res,sv+E[ub].v1);
}
}
printf("%lld",res);
return 0;
}