挑战程序设计竞赛(第三章:3.2 常用技巧精选(一))

文章目录

        • 尺取法
          • Subsequence(POJ 3061)
          • Jessica's Reading Problem(POJ 3320)
        • 反转(开关问题)
          • Face The Right Way(POJ 3276)
          • Fliptile(POJ 3279)
        • 碰撞问题
          • Physics Experiment(POJ 3624)
        • 折半枚举
          • 4 Values whose Sum is 0(POJ 2785)
          • 超大0-1背包问题
          • Subset(POJ 3977)

尺取法

数据需要具有单调性,才能使用尺取法。

Subsequence(POJ 3061)

题目链接:Subsequence
参考博文:挑战程序设计竞赛: Subsequence

  • 题目大意:给定长度为n的数列整数入a0, a1 … an以及整数S,求出总和不小于S的连续子序列的长度的最小值。如果解不存在,则输出0。
  • 思路: 两种方法,二分和尺取法,参考博文。

代码:

/* 尺取法 */
#include 
#include 
#include 
using namespace std;

const int MAX = 100005;
int T, N, S, a[MAX];

int main()
{
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d", &N, &S);
        for(int i=0; i<N; i++)
        {
            scanf("%d", &a[i]);
        }
        int s = 0, t = 0, sum = 0, res = N+1;
        while(1)
        {
            while(t<N && sum<S)//找到满足条件的一个尺度
            {
                sum += a[t++];
            }
            if(sum<S) break;
            res = min(res, t-s);
            sum -= a[s++];
        }
        if(res>N)
            printf("0\n");
        else
            printf("%d\n", res);
    }
    return 0;
}
Jessica’s Reading Problem(POJ 3320)

题目链接:Jessica’s Reading Problem

  • 题目大意:XXX要准备考试,书总共有P页,第i页恰好有一个知识点ai,书中的同一个知识点可能会被多次提到,所以他希望看其中连续的一些页的书来把所有的知识点都给看完。求最少阅读页数。
  • 思路:尺取法。尺取法的主要用途是求一个序列的某个目标区间长度。该题的目标是最短的将所有数字均包括的区间,只需要不断更新尺取法的起始点,然后找到在该起始点基础上的最短尺度,通过比较不同起始点的最短尺度即可得出答案。

代码:

#include 
#include 
#include 
using namespace std;

const int MAX = 1000005;
const int INF = 1<<29;
int a[MAX], n, P, s, e, Min, sum;
map<int, int> num;

int main()
{
    n = 0;
    scanf("%d", &P);
    for(int i=0; i<P; i++)
    {
        scanf("%d", &a[i]);
        if(num.count(a[i])==0)
        {
            num[a[i]] = 0;
            n++;
        }
    }
    s = 0, e= 0, Min = INF, sum = 0;
    while(1)
    {
        while(sum<n && e<P)
        {
            if(num[a[e]]==0)
            {
                sum++;
                num[a[e]] = 1;
                e++;
            }
            else
            {
                num[a[e]] = num[a[e]]+1;
                e++;
            }
        }
        if(sum<n) break;
        Min = min(e-s, Min);
        if(num[a[s]]==1)
            sum--;
        num[a[s]] = num[a[s]]-1;
        s++;
    }
    printf("%d\n", Min);
    return 0;
}

反转(开关问题)

Face The Right Way(POJ 3276)

题目链接:Face The Right Way
参考博文:POJ-3276:Face The Right Way

  • 题目大意:N头牛有的朝前有的朝后,调转牛方向的机器每次只能且必须改变相邻的K头牛,改变后牛的方向与之前相反,要求最后调转得全部朝前。
    输入:N代表牛数量,F代表初始朝前,B代表初始朝后。
    输出:最小的调转次数对应的K值,最小的调转次数M。
  • 思路:枚举+有条件的遍历。首先需要明确一次需要旋转[i, i+k-1]的区间,因此每一次判断是否旋转时需要判断的是第i个是否旋转,第i个方向转好后,再旋转下一个j(j>i)的位置。判断是否旋转需要明确一个牛如果初始方向相反,则需要旋转奇数次,否则旋转偶数次,所以需要一个变量存储在比遍历到i时第i个位置已经旋转的次数,并且第i个位置的旋转次数只与[max(0,i-k+1),i-1]区间内有关系,需要不断更新该变量。最终需要判断整个区间是否已经方向统一。

代码:

#include 
#include 
#include 
using namespace std;

const int MAX = 5005;
const int INF = 1<<29;
//K和M是输出的长度和翻转次数,dir是初始方向,f是该位置是否翻转(0和1)
int N, K, M, dir[MAX], f[MAX];
char ch;

int solve(int k)
{
    memset(f, 0, sizeof(f));//初始均为没有翻过
    int sum = 0, res = 0;//sum当前i的翻过的次数,res翻过的总次数
    for(int i=0; i<N-k+1; i++)//在可以旋转的区间内遍历
    {
        if((dir[i]+sum)%2)//表明该位置翻转后面朝后,需要再次翻转
        {
            res++;
            f[i] = 1;
        }
        //更新sum
        sum += f[i];
        if(i-k+1>=0)//sum一直记录遍历到i时,i已经翻转的次数
            sum -= f[i-k+1];
    }
    for(int i=N-k+1; i<N; i++)//剩下的无法再旋转,则判断剩下的是否已经满足了方向
    {
        if((dir[i]+sum)%2)//面朝后
        {
            return -1;
        }
        if(i-k+1>=0)
            sum -= f[i-k+1];
    }
    return res;
}

int main()
{
    scanf("%d", &N);
    for(int i=0; i<N; i++)
    {
        getchar();
        scanf("%c", &ch);
        if(ch=='F')
            dir[i] = 0;
        else
            dir[i] = 1;
    }
    //枚举
    M = INF;
    for(int k=1; k<=N; k++)
    {
        int m = solve(k);
        if(m>=0 && m<M)
        {
            M = m;
            K = k;
        }
    }
    printf("%d %d\n", K, M);
    return 0;
}
Fliptile(POJ 3279)

题目链接:Fliptile
参考博文:Fliptile POJ - 3279(超详解)

  • 题目大意:给你一个M*N的棋盘,1代表翻转,0代表不翻转,问你怎样才能反转最少的次数让1都变成0,你的每次翻转都会翻转它的上下左右棋子,最后输出你翻转的方式,如果有多种方式,输出字典序最小的,如果不能完成翻转,输出"IMPOSSIBLE"。
  • 思路:需要明确的是我们在判断是否翻转(i, j)时,实则是为了保证(i-1, j)一定时白色,即当前行是为了上一行的同一列的位置而服务的。所有大致的思路如下:
    1)枚举第一行的所有翻转排列情况,按照字典序从小到大来枚举(更新时需要保证小于上一个的翻转次数才更新)。
    2)从第二行(直到最后一行)开始判断是否翻转,依据是上一行的状态,目标是使得上一行同一列的元素白色朝上。
    3)判断最后一行是否均白色朝上,否则失败。
    4)判断是否符合更新条件。
    还有一个需要讨论的是如何标记一个元素的状态,当元素黑色朝上时,需要翻转奇数次,否则翻转偶数次,因此需要用数组存储一个元素的翻转次数,即与其相邻的翻转时,其一定翻转。

代码:

#include 
#include 
#include 
using namespace std;

const int MAX = 20;
const int INF = 1<<29;
int N, M, cnt, res;
int G[MAX][MAX], turn[MAX][MAX], ans[MAX][MAX];
int dx[5] = {0, 1, 0, -1, 0};
int dy[5] = {1, 0, -1, 0, 0};

//通过自身的状态和周围的turn(翻转次数)来判断(x,y)是否翻转
int getColor(int x, int y)
{
    int temp = G[x][y];
    for(int i=0; i<5; i++)
    {
        int xi = x+dx[i], yi = y+dy[i];
        if(xi>=0 && xi<M && yi>=0 && yi<N)
            temp += turn[xi][yi];
    }
    return temp%2;
}

//第i层的(i, j)是否翻转取决于(i-1, j)是否需要翻转
void solve()
{
    for(int i=1; i<M; i++)
    {
        for(int j=0; j<N; j++)
        {
            if(getColor(i-1, j))//翻转
            {
                turn[i][j] = 1;
                cnt++;
            }
            if(cnt>res) return;//翻转次数已经超出,减枝
        }
    }
    //检测是否成功
    for(int i=0; i<N; i++)
        if(getColor(M-1, i)) return;
    //更新
    if(cnt<res)
    {
        res = cnt;
        memcpy(ans, turn, sizeof(turn));
    }
}
int main()
{
    res = INF;
    scanf("%d%d", &M, &N);
    for(int i=0; i<M; i++)
    {
        for(int j=0; j<N; j++)
        {
            scanf("%d", &G[i][j]);
        }
    }
    //第一层的翻转情况全排列一下,按字典序
    for(int i=0; i<1<<N; i++)//2^N中情况,集合的整数表现
    {
        cnt = 0;
        memset(turn, 0, sizeof(turn));
        for(int j=0; j<N; j++)
        {
            turn[0][N-1-j] = i>>j&1;//得到排列的值
            if(turn[0][N-1-j]) cnt++;
        }
        solve();
    }
    if(res==INF) printf("IMPOSSIBLE\n");
    else
    {
        for(int i=0; i<M; i++)
        {
            for(int j=0; j<N; j++)
            {
                if(j>0) printf(" ");
                printf("%d", ans[i][j]);
            }
            printf("\n");
        }
    }
    return 0;
}

碰撞问题

Physics Experiment(POJ 3624)

题目链接:Physics Experiment
参考博文:Greedy:Physics Experiment(弹性碰撞模型)(POJ 3848)

  • 题目大意:有一个与地面垂直的管子,管口与地面相距H,管子里面有很多弹性球,从t=0时,第一个球从管口求开始下落,然后每1s就会又有球从球当前位置开始下落,球碰到地面原速返回,球与球之间相碰会发生完全弹性碰撞(各自方向改变,速率变为相碰时另一个球的速率)问最后所有球的位置?
  • 思路:类似于Ants题目,区别在于需要考虑球的半径。但是我们可以先不考虑球的半径,则就相当于同一高度下落,只是释放时间不同。因为球的顺序不会改变,所以按从小到大排序,可得顺序高度。然后我们可以将按序加上2Ri的高度(原因是除了第一个可以触底外,其他的求的最小范围均为2Ri),而在球相互撞击时这个距离根本不需要考虑,顶部撞击底部就可以交换速度。

代码:

#include 
#include 
#include 
#include 
using namespace std;

const int MAX = 105;
const double g = 10.0;
int R, H, N, T, C, Case;
double h[MAX];
double solve(int h, int t)
{
    if(t<0) return h;//可能还没有释放
    double t1, t2;
    t1 = sqrt(2*h/g);
    int k = (int)(t/t1);
    if(k%2)
    {
        t2 = k*t1+t1-t;
        return h-g*t2*t2/2;
    }
    else
    {
        t2 = t-k*t1;
        return h-g*t2*t2/2;
    }
}
int main()
{
    scanf("%d", &Case);
    while(Case--)
    {
        scanf("%d%d%d%d", &N, &H, &R, &T);
        for(int i=0; i<N; i++)
        {
            h[i] = solve(H, T-i);
        }
        sort(h, h+N);
        for(int i=0; i<N; i++)
        {
            if(i!=0) printf(" ");
            printf("%.2f", h[i]+2*R*i/100.0);
        }
        printf("\n");
    }
    return 0;
}

折半枚举

4 Values whose Sum is 0(POJ 2785)

题目链接:4 Values whose Sum is 0

  • 题目大意:输入四个集合,从四个集合中分别选出四个数,求使之和为0的组合数。
  • 思路:将四个集合分为两个部分。第一个部分是前两个集合,求前两个集合的各个元素组合的和,然后排好序。第二个部分,求后两个集合的元素组合的和,然后在第一个部分的排好序的数组中找到与之相加为0的组合数目。一直累加即可得到结果。

代码:

#include 
#include 
#include 
#include 
using namespace std;

typedef long long LL;
const int MAX = 4005;
map<int, int> m;
int N, A[MAX], B[MAX], C[MAX], D[MAX], AB[MAX*MAX];
int main()
{
    LL ans = 0;
    scanf("%d", &N);
    for(int i=0; i<N; i++)
    {
        scanf("%d%d%d%d", &A[i], &B[i], &C[i], &D[i]);
    }
    for(int i=0; i<N; i++)
    {
        for(int j=0; j<N; j++)
        {
            AB[i*N+j] = A[i]+B[j];
        }
    }
    sort(AB, AB+N*N);

    for(int i=0; i<N; i++)
    {
        for(int j=0; j<N; j++)
        {
            int cd = -C[i]-D[j];
            ans += upper_bound(AB, AB+N*N, cd)-lower_bound(AB, AB+N*N, cd);
        }
    }
    printf("%lld\n", ans);
    return 0;
}
超大0-1背包问题

题目:
挑战程序设计竞赛(第三章:3.2 常用技巧精选(一))_第1张图片
思路:
挑战程序设计竞赛(第三章:3.2 常用技巧精选(一))_第2张图片
代码:

#include 
#include 
#include 
using namespace std;

typedef long long LL;
const LL INF = 1<<31;
const int MAX = 45;
int n;
LL w[MAX], v[MAX], W;
pair<LL, LL> pi[1<<(MAX/2)];

void solve()
{
    //枚举前半部分
    int n2 = n/2;
    //使用二进制排列枚举(重点)
    for(int i=0; i<(1<<n2); i++)
    {
        LL sw = 0, sv = 0;
        for(int j=0; j<n2; j++)
        {
            if((i>>j)&1)
            {
                sw += w[i];
                sv += v[i];
            }
        }
        pi[i] = make_pair(sw, sv);
    }

    sort(pi, pi+(1<<n2));
    int m = 1;
    for(int i=1; i<(1<<n2); i++)//去除多余的元素
    {
        if(pi[m-1].second<pi[i].second)
        {
            pi[m++] = pi[i];
        }
    }

    //枚举后半部分并求解
    LL res = 0;
    for(int i=0; i<(1<<(n-n2)); i++)
    {
        LL sw = 0, sv = 0;
        for(int j=0; j<n-n2; j++)
        {
            if((i>>j)&1)
            {
                sw += w[n2+j];
                sv += v[n2+j];
            }
        }
        if(sw<=W)
        {
            //搜索前半部分的结果,得到不大于W-sw对应的sv
            LL tv = (upper_bound(pi, pi+m, make_pair(W-sw, INF))-1)->second;
            res = max(res, sv+tv);
        }
    }
    printf("%lld\n", res);
}
int main()
{
    while(scanf("%d%lld", &n, &W)!=EOF)
    {
        for(int i=0; i<n; i++)
        {
            scanf("%lld%lld", &w[i], &v[i]);
        }
        solve();
    }
    return 0;
}
Subset(POJ 3977)

题目链接:Subset
参考博文:POJ 3977Subset(枚举+二分)

  • 题目大意:让你从n个数里面找任意个数(>0),使他们的和的绝对值最小,如果有多组和一样最小,输出最小和的绝对值且最小个数。
  • 思路:其实思路大致和超大0-1背包问题相同,均是折半枚举+二分。将n个数分成两个部分,前半部分枚举所有情况(和),并排序,然后在后半部分枚举和的同时二分查找前半部分中满足情况的值。只不过稍微复杂一点。

代码:

#include
#include
#include
#include
#include
#include
#include

#define ll long long
using namespace std;
const int maxn=1000005;

int n,m,p;
ll a[35];

struct node
{
    ll val;
    int len;
    node(ll val=-1, int len=-1): val(val), len(len){}
    bool operator < (const node &A) const
    {
        if(val==A.val) return len<A.len;
        return val<A.val;
    }
} nod1[maxn],nod[maxn];

int erfen(ll x)
{
    int l=0,r=p-1,mid;
    while(r>=l)
    {
        mid=(l+r)>>1;
        if(nod[mid].val>=x) r=mid-1;
        else l=mid+1;
    }

    return l;
}

int cmp(node p1,node p2)
{
    if(p1.val<p2.val) return 1;
    if(p1.val==p2.val&&p1.len<p2.len) return 1;
    return 0;
}

int main()
{
    int i;

    int x,s,res;
    ll ans;
    while(cin>>n&&n)
    {
        for(i=0; i<n; i++)
            cin>>a[i];

        ans=1e15+5,res=50;
        m=n/2;  //前面用来枚举,后面用来二分.

        s=1<<(n-m);//后面的数据
        for(x=1; x<s; x++) //建立用于二分的数据,二进制划分
        {
            int tt=x;
            ll tmp=0;

            int cnt=0;
            for(i=m; i<n; i++)//下标
            {
                if(tt&1)
                {
                    tmp+=a[i];
                    cnt++;
                }
                tt>>=1;
            }

            nod1[x].val=tmp;
            nod1[x].len=cnt;
        }

        sort(nod1+1,nod1+s,cmp);

        nod[0].val=0;
        nod[0].len=0;
        nod[1].val=nod1[1].val;
        nod[1].len=nod1[1].len;

        //去除多余的元素
        p=2;
        for(i=2;i<s;i++)
        {
            if(nod1[i].val!=nod[p-1].val)
            {
                nod[p].val=nod1[i].val;
                nod[p++].len=nod1[i].len;
            }
        }

        sort(nod,nod+p,cmp);

        //枚举
        s=1<<m;
        for(x=0; x<s; x++)
        {
            ll tt=x,tmp=0;

            int cnt=0;
            for(i=0; i<m; i++)
            {
                if(tt&1)
                {
                    tmp+=a[i];
                    cnt++;
                }
                tt>>=1;
            }

            int pos = lower_bound(nod, nod+p, node(-tmp, 0))-nod;
            //int pos=erfen(-tmp);
            int pos1=max(pos-1,0),pos2=min(pos+1,p-1);

            //在一定范围内筛选出符合要求的解
            for(i=pos1; i<=pos2; i++)
            {
                ll tmp1=nod[i].val+tmp;
                int tmp2=nod[i].len+cnt;
                if(tmp1==0&&tmp2==0) continue;  //不能什么都不取
                if(tmp1<0) tmp1=-tmp1;
                if(tmp1<ans||(tmp1==ans&&tmp2<res))//条件
                {
                    ans=tmp1;
                    res=tmp2;
                }
            }
        }

        cout<<ans<<" "<<res<<endl;
    }
    return 0;
}

你可能感兴趣的:(挑战程序设计竞赛,挑战程序设计竞赛——经验篇)