挑战程序设计竞赛  反转(开关问题) poj3276

题目描述


N头牛排成一列。每头牛或者向后或者向前。为了让所有牛都面向前方,农夫约翰买了一台自动转向的机器。这个机器在购买时就必须设定一个数值K,机器每操作一次恰好使K头连续的牛转向。请求出为了让所有的牛都能面向前方所需要的最少操作次数M及其对应的K。
限制条件:1<=N<=5000


解题思路


  首先,题目要求我们找出最小的操作次数(及其对应的K值)使得所有的牛都面向前方。由题目可以得知,1<=K<=N 。当K给定时,我们可以求得相应的操作次数。这其中操作次数的最小值及其对应的K值就是所求解。所以接下来我们只需知道如何求解当K值给定时的对应操作次数。
  我们知道每次操作都是对一个区间范围内的牛进行转向操作。区间操作的顺序并不影响结果,对区间进行两次或两次以上的操作是没有意义的。所以对每一个区间,我们都需要判断他是否需要进行操作,所有需要进行操作的区间的个数即相应的M值。接着问题就转化成了如何判断一个区间需不要进行转向操作。
  由于一个位置可以属于若干不同的区间,区间操作的结果相互影响,无法独立的去判断一个区间需不需要进行转向操作。但是我们可以发现最左端的牛只能属于一个区间,这就是说如果最左端的牛一开始是面向前方的那么就一定不能对这个区间进行转向操作。反之,如果最左端的牛一开始是面向后方的那么就必须对这个区间进行转向操作。当确定最左端的区间需不需要转向之后,次左段的牛的当前方向也就定下来了,我们可以根据这个当前方向确定左端第二个区间需不需要转向。以此类推,一个牛一个牛的考虑下去,就能依次确定每个区间需不需要进行操作了。由此,可解出答案。

  

方法设计

  我们可以简单的计算一下这个算法的复杂度。首先,我们需要遍历一次K所有可能的取值。其次是依次对每个位置,计算以该位置为首的区间需不需要进行转向操作。接着,对每次区间的操作都需要反转K头牛。总的复杂度达到了O(N^3)。这样会导致计算时间超时,所以需要优化算法。而区间转向操作的那个部分是可以优化的。
  在实现过程中,我们并不需要实际的去改变每头牛的方向,然后在这基础上去进行后续的操作。可以通过计算来确定。
  我们可以设置一个标记数组flag[]。用flag[i]标记以i为首的区间需不需要进行转向操作。1表示转向,0表示不转向。
  在此基础上,我们还需要一个初始的方向数组dir[]。dir[i]表示牛的初始朝向。1表示牛面向后方,0表示牛面向前方。
  通过前面的设定,我们可以发现在判断第i个区间需不需要进行转向时,我们可以通过flag[i-1],flag[i-2]…,flag[i-k+1]以及dir[i]的值来判断。dir[i]的值表示了初始朝向,而flag[i-1],flag[i-2]…,flag[i-k+1]的值则告诉我们之前所有包含了i位置的区间有没有进行转向,从而可以计算出在之前的那些操作之后,i位置牛的方向。这样就不需要通过一步一步的实际操作来记录牛的方向。由于0和1的特殊性,我们发现在之前那个设定的情况下,计算flag[i-1],flag[i-2]…,flag[i-k+1]以及dir[i]的叠加和,若该值是奇数,则该牛当前方向为朝后,需要转向。反之,则不需要。而flag[i-1],flag[i-2]…,flag[i-k+1]的和并不需要用循环去计算,只需要在计算过程中步步记录即可。这样循环就缩小到了二重,计算不会超时。
  这部分的说明如果看不懂,可以结合代码去看,感觉结合代码更好懂一点。书上的优化说明那边直接看的话,我就有点懵。
  

#include<iostream>
#include<string>
using namespace std;
const int Max_N = 5000;
int N;
int M,K;
char input[Max_N];
int dir[Max_N];//表示每头牛的朝向
int flag[Max_N];//flag[i]标志以第i头牛为首的k个位置的牛是否需要转向 1表示需要转向 0表示不需要转向
int calc(int k)
{
    fill(flag,flag+N,0);//初始化标志数组
    int res = 0;
    int sum = 0;  //sum=flag[i-1]+flag[i-2]+...+flag[i-k+1]记录在没有计算到第i头牛时,第i头牛被连带转向了几次
    for (int i=0;i+k<=N;i++)
    {
        if((dir[i]+sum)%2!=0)//表示第i头牛正面向后方,需要转向
        {
            res++;
            flag[i]=1;
        }

        sum += flag[i];
        if(i-k+1>=0)
        {
            sum -= flag[i-k+1];
        }
    }

    for (int i=N-k+1;i<N;i++)
    {
        if((dir[i]+sum)%2!=0)//表示第i头牛正面向后方,需要转向
        {
            return -1;
        }
        if(i-k+1>=0)
        {
            sum -= flag[i-k+1];
        }
    }

    return res;


}
int main()
{
    cin>>N;
    int K = 1;  //连续转向的牛的个数
    int M = N+1;  //使所有牛都朝向前方所需的最小转向次数
    for (int i=0;i<N;i++)
    {
        cin>>input[i];   //将表示前后方的字符用0,1表示 
        if(input[i]=='B')
        {
            dir[i]=1;
        }
        else
        {
            dir[i]=0;
        }
    }
    int m,k;
    for (k=1;k<=N;k++)
    {
        m = calc(k);
        if(m<M&&m>0)
        {
            M = m;
            K = k;
        }
    }
    cout<<K<<" "<<M<<endl;
    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值