几种做题时的思考方法

二分答案(最大化平均值)

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=1kwii=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=1kwii=1kvix
将不等式变形得到
∑ i = 1 k ( v i − x × w i ) ≥ 0 \sum_{i=1}^k (v_i-x\times w_i)\ge 0 i=1k(vix×wi)0
对( v i − x × w i v_i-x\times w_i vix×wi)的值进行排序贪心的选取
C(x)=( v i − x × w i v_i-x\times w_i vix×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=H21g(Tkt)2(k
y = H − 1 2 g ( k t + t − T ) 2 ( k 是 奇 数 时 ) y=H-\frac 12g{(kt+t-T)}^2(k是奇数时) y=H21g(kt+tT)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 1n4000
⋅ ∣ 数 字 的 值 ∣ ≤ 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 1n40
⋅ 1 ≤ w i , v i ≤ 1 0 15 \cdot1\le w_i,v_i\le10^{15} 1wi,vi1015
⋅ 1 ≤ W ≤ 1 0 15 \cdot1\le W\le10^{15} 1W1015

使用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;
} 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值