3.2常用技巧(一)

来源:互联网 发布:沙盒软件 排名 编辑:程序博客网 时间:2024/05/19 16:05

3.2.1尺取法

尺取法通常指对数组保存一对下标(起点,终点),然后根据实际情况交替推进两个端点直到得出答案的方法。

eg Subsequence

给定长度为N的数列整数a0,a1,,,,,an-1以及整数S。求出总和不小于S的连续子序列的长度的最小值。如果不存在输出0

int n,S;int a[MAX_N];int sum[MAX_N+1];void solve(){    //计算sum    for(int i=0;i<n;i++)    {        sum[i+1]=s[i]+a[i];    }    if(sum[n]<S)    {        //解不存在        printf("0\n");        return;    }        int res=n;    for(int s=0;sum[s]+S<=sum[n];s++)    {        //利用二分搜索求出T        int t=lower_bound(sum+s,sum+n,sum[s]+S)-sum;        res=min(res,t-s);    }    printf("%d\n",res);}
虽然以及足以解决这个问题,但我们还可以更加高效地求解。我们设以as开始总和最初大于S时的连续子序列为as+...+at-1,这时

as+1+...+at-2<as+...+at-2<S

所以从as+1开始总和最初超过S的连续子序列如果是as+1+...+at‘-1的话,则必然有t<=t'.利用这一性质便可以设计出如下算法

(1)以s=t=sum=0初始化

(2)只要依然有sum<S,就不断将sum增加at,并将t增加1

(3)如果(2)中无法满足sum>S,则终止,否则的话,更新res=min(res,t-s)

(4)将sum减去as,s增加1然后回到(2)

对于这个算法,因为T最多变化n次,因此只需O(n)的复杂度就可以求解这个问题了

void solve(){    int res=n+1;    int s=0,t=0,sum=0;    for(;;)    {        while(t<n&&sum<S)        {            sum+=a[t++];        }        if(sum<S)            break;        res=min(res,t-s);        sum=sum-a[s++];    }    if(res>n)    {        res=0;    }    printf("%d\n",res);}

像这样反复地推进区间的开头和末尾,来求取满足条件的最小区间的方法被称为尺取法

eg Jessica's Reading Problem

我们假设从某一页s开始阅读,为了覆盖所有的只是点需要阅读到t。这样的话可以知道如果从s+1开始阅读的话,那么必须阅读到t'>t页为止。由此这题也可以使用尺取法

在某个区间[s,t]已经覆盖了所有的知识点的情况下,下一个区间【s+1,t'】要如何求出呢

“所有的知识点都被覆盖--每个知识点出现的次数不小于1 ”

由以上的等价关系。我们可以用二叉树等数据结构来存储[s,t]区间上每个知识点的出现次数,这样把最开头的页S去除之后便可以判断[s+1,t]是否满足条件

从区间的最开头把S取出之后,页S上书写的知识点的出现次数要减一,如果此时这个知识点出现次数为0了,在同一个知识点再次出现前,不停将区间末尾t向后推进即可。每次在区间末尾追加也t时将页t上的知识点的出现次数+1,这样就完成了下一个区间上各个知识点出现次数的更新。

int P;int a[MAX_P];void solve(){    //计算全部知识点的总数    set<int>all;    for(int i=0;i<P;i++)    {        all.insert(a[i]);    }    int n=all.size();        //利用尺取法来求解    int s=0,t=0.num=0;    map<int,int>count;   //知识点->出现次数的映射    int res=P;    for(;;)    {        while(t<P&&num<n)        {            if(count[a[t++]]++==0)            {                num++;            }        }        if(num<n)            break;        res=min(res,t-s);        if(--count[a[s++]]==0)        {            //某个知识点的出现次数为0了            num--;        }    }    printf("%d\n",res);}

3.2.2反转(开关问题)

eg Face The Right Way

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


首先,交换区间反转的顺序对结果没有影响。此外,可以知道对同一个区间进行两次的反转是多余的。由此,问题就转化成了求需要被反转的区间的集合。于是我们先考虑一下最左端的牛。包含这头牛的区间只有一个,因此如果这头牛面朝前方。我们就能知道这个区间不需要反转

反之,如果这头牛面朝后方,对应的区间就必须进行反转了。而且在此之后最左边的区间就不需要考虑。问题的规模就缩小了1.不断重复下去无需搜索求出最少所需的反转次数了

此外,通过上面的分析可以知道,忽略掉对同一个区间重复反转这类多余操作之后,只要存在让所有的牛都朝前的方法,那么操作就和顺序无关可以唯一确定了。

首先我们需要对所有的K都求解一次,对于每个K我们都要从最左端来考虑N头牛的情况。此时最坏的情况下需要进行N-K+1的反转操作,而每次操作又要反转K头牛,于是总的复杂度就是N^3.这样的话时间很长,但是区间反转那里可以优化

f[i]:=区间[i,i+k-1]进行了反转的话则为1,否则为0

这样,如果在考虑第i头牛是,如果。。f[i]为奇数的话,则这头牛的方向与起始方向是相反的,否则方向不变。由于

int N;int dir[MAX_N];   //牛的方向(0:F,1:B)int f[MAX_N];     //区间[i,i+k-1]是否进行反转//固定K,求对应的最小操作回数//无解的话则返回-1int calc(int K){    memset(f,0,sizeof(f));    int res=0;    int sum=0; //f的和    for(int i=0;i+K<=N;i++)    {        //计算区间[i,i+K-1]        if((dir[i]+sum)%2!=0)        {            //前端的牛面向后方            res++;            f[i]=1;        }        sum+=f[i];        if(i-K+1>=0)        {            sum-=f[i-K+1];        }    }    //检查剩下的牛是否有面朝后方的情况    for(int i=N-K+1;i<N;i++)    {        if((dir[i]+sum)%2!=0)        {            return -1;        }        if(i-K+1>=0)        {            sum-=f[i-K+1];        }    }    return res;}void solve(){    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\n",K,M);}

eg Fliptile

农夫约翰知道聪明的牛产奶多。于是为了提高牛的智商他准备了如下游戏。有一个M*N的格子,每个格子可以翻转正反面,它们一面是黑色,另一面是白色。黑色的格子翻转后就是白色,白色的格子翻转过来则是黑色。游戏要做的就是把所有的格子翻成白色。求最小步数完成是每个格子翻转的次数。最小步数的解有多个,输出字典序最小的一组


首先,同一个格子翻转两次会回复原状,所以多次翻转是多余的,此外,翻转的格子的集合相同的话,其次序是无关紧要的。因此,总共有,,种翻法

不妨先指定好最上面一行的翻转方法。此时能够翻转(1,1)的只剩(2,1),所以可以直接判断(2,1)是否需要翻转。类似的(2,1)~(2,N)都能这样判断,如此反复下去就可以确定所有格子的翻转方法。最后如果并非全为白色,就意味着不可能。

像这样,先确定第一行的翻转方式,然后可以很容易判断这样是否存在解以及解的最小步数是多少,这样将第一行的所有翻转方式都尝试一次就能求出这个问题最小步数

//邻接的格子的坐标const int dx[5]={-1,0,0,0,1};const int dy[5]={0,-1,0,1,0};//输入int M,N;int tile[MAX_M][MAX_N];int opt[MAX_M][MAX_N];   //保存最优解int flip[MAX_N][MAX_N];  //保存中间结果//查询(x,y)的颜色int get(int x,int y){    int c=tile[x][y];    for(int d=0;d<5;d++)    {        int x2=x+dx[d],y2=y+dy[d];        if(0<=x2&&x2<M&&0<=y2&&y2<N)        {            c+=flip[x2][y2];        }    }    return c%2;}//求出第1行确定情况下的最小操作次数//不存在解的话返回-1int calc(){    //求出从第2行开始的翻转方法    for(int i=1;i<M;i++)    {        for(int j=0;j<N;j++)        {            if(get(i-1,j)!=0)            {                //(i-1,j)是黑色的话,必须翻转这个格子                flip[i][j]=1;            }        }    }        //判断最后一行是否全白    for(int j=0;j<N;j++)    {        if(get(M-1,j)!=0)        {            return -1;        }    }        //统计翻转的次数    int res=0;    for(int i=0;i<M;i++)    {        for(int j=0;j<N;j++)        {            res+=flip[i][j];        }    }    return res;}void solve(){    int res=-1;    //按照字典序尝试第一行的所有可能性    for(int i=0;i<1<<N;i++)    {        memset(flip,0,sizeof flip);        for(int j=0;j<N;j++)        {            flip[0][N-j-1]=i>>j&1;        }        int num=calc();        if(num>=0&&(res<0||res>num))        {            res=num;            memcpy(opt,flip,sizeof flip);        }    }    if(res<0)    {        //无解        printf("IMPOSSIBLE\n");    }    else    {        for(int i=0;i<M;i++)        {            for(int j=0;j<N;j++)            {                printf("%d%c",opt[i][j],j+1==N?'\n':' ');            }        }    }}



0 0