程序設計競賽常用技巧精選

對《挑戰程序設計競賽》的一個記錄

第三章 出類拔萃——中級篇

3.2 常用技巧精選

(1)尺取法


poj 3061 Subsequence
給定長度爲n的數列整數a0,a1,…an-1以及證書S。求出總和不小於S的連續子序列的長度的最小值。如果解不存在在,則輸出0.
已知:
10< n< 10^5
0< ai 10^4
S< 10^8

sample input
n = 10
S = 15
a = {5,1,3,5,10,7,4,9,2,8}
sample output
2 (5 + 10)


這題比較好想的一個思路就是先求出前n項的和,再在滿足sum>=S的時候,val = sum - S,二分查找之前的項中滿足≤val的最大項。
例如:

index: 0 1 2 3 4 5 6 7 8 9
a : 5 1 3 5 10 7 4 9 2 8
sum: 5 6 9 14 24 31 35 44 46 54

第一個滿足sum >= 15的值是下標爲4的值,以此爲例,前5項的和爲24,val = 24 - 15 = 9,我要找前5項中sum<=9的最大小標,可以找到是小標爲2的值,因此可得到連續子序列最小的長度爲4 - 2 = 2 即5 + 10
這個算法的時間複雜度爲:O(nlogn )
代碼如下:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>

#define sf scanf
#define pf printf

using namespace std;

const int Maxn = 100010;
int T,n,s;
int sum[Maxn];
int solved(int l,int r,int k)
{
    while(l <= r)
    {
        int mid = (l + r) / 2;
        if(sum[mid] <= k)
            l = mid + 1;
        else
            r = mid - 1;
    }
    return r;
}
int main()
{
    int a;
    sf("%d",&T);
    while(T--)
    {
        sf("%d%d",&n,&s);
        for(int i = 0;i < n;i ++)
        {
            sf("%d",&a);
            if(i == 0) sum[i] = a;
            else sum[i] = sum[i - 1] + a;
        }
        int Min = n + 1;
        for(int i = 0;i < n;i ++)
        {
            if(sum[i] >= s)
            Min = min(Min,i - solved(0,i,sum[i] - s));
        }
        pf("%d\n",Min > n?0:Min);
    }
    return 0;
}

那什麼是尺取法?尺取法能更高效地解決此類問題。
我們設以as 開始總和最初大於S時的連續子序列as+...+at1 ,這時
as+1+...+at2<as+...+at2<S
所以從as+1 開始總和最初超過S的連續子序列如果是as+1+...+at1 的話,則必然有tt
用下面的圖來解釋比較清晰:

這裏寫圖片描述

代碼如下:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>

#define sf scanf
#define pf printf

using namespace std;

const int Maxn = 100010;
int T,n,s;
int sum[Maxn];
int main()
{
    int a;
    sf("%d",&T);
    while(T--)
    {
        int tail = -1,head = -1;
        sf("%d%d",&n,&s);
        for(int i = 0;i < n;i ++)
        {
            sf("%d",&a);
            if(i == 0) sum[i] = a;
            else sum[i] = sum[i - 1] + a;
            if(tail == -1 and sum[i] >= s)
                tail = i;
        }
        if(tail == -1)
        {
            pf("0\n");
            continue;
        }
        int Min = n;
        while(head < tail)
        {
            if(sum[tail] - sum[head + 1] >= s)
            {
                head ++;
                Min = min(Min,tail - head);
            }
            else if(tail < n - 1) tail++;
            else
                break;
        }
        pf("%d\n",Min);


    }
    return 0;
}

尺取法的算法複雜度爲O(n)

兩個程序結果比較如下:(第一行爲尺取法,第二行爲二分的方法)
第一行爲尺取法,第二行爲二分的方法

poj 3320 Jessica’s Reading Problem

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <map>

#define sf scanf
#define pf printf

using namespace std;

const int Maxn = 1000010;
map<int,int> mp,tmp;
int p[Maxn];
int n;
int main()
{
    while(~sf("%d",&n))
    {
        mp.clear();
        tmp.clear();
        for(int i = 0;i < n;i ++)
        {
            sf("%d",&p[i]);
            mp[p[i]]++;
            tmp[p[i]]++;
        }
        int s = mp.size();
        int tail = 0, head = 0;
        int num = 1,Min = n;
        tmp[p[0]]--;
        while(head <= tail)
        {
            if(num < s && tail < n - 1)
            {
                tail ++;
                if(tmp[p[tail]] == mp[p[tail]])
                    num ++;
                tmp[p[tail]] --;
            }
            else if (num == s)
            {
                Min = min(Min,tail - head + 1);
                tmp[p[head]] ++;
                if(tmp[p[head]] == mp[p[head]])
                    num --;
                head ++;
            }
            else
                break;
        }
        pf("%d\n",Min);

    }
    return 0;
}

(2) 反轉(開關問題)


POJ 3276 Face The Right Way
N頭牛排列成了一列,每頭牛或者向前或者向後站,爲了讓所有的牛都面向前方,農夫約翰買了一臺自動轉向的機器,這個機器在購買時就必須設定一個數值K,機器每操作一次恰好使K頭連續的牛轉向(K頭牛分別爲當前的牛及其之後的牛,不影響位於它之前的牛,並且每次反轉必須是K頭牛,不可以少於K頭)。請求出爲了讓所有的牛都能面向前方需要的最少的操作次數M和對應的最小的K。
已知:
1 N 5000

sample input
N = 7
BBFBFBB(F:面向前方,B:面向後方)

sample output
K = 3
M = 3
(先反轉1~3號的三頭牛,然後再反轉3~5號,最後反轉5~7號)


這裏寫圖片描述

這題還算比較好做,主要有個條件是從當前牛開始,與位於其後的共K頭牛進行反轉。判斷第i頭牛是否需要反轉,只需要根據能影響到它的第 i - K+1,…,i - 1頭牛的反轉情況就可以確定了。

f[i]:=區間[i,i + K - 1]進行了反轉的話則爲1,否則爲0。
sum = f[i - k + 1] + f[i - k + 2] + …+f[i - 1];

如果sum爲奇數,表明第i頭牛被反轉了,如果原來的牛是面向後方的(B),則被轉成面向前方了,f[i] = 0,不用繼續反轉了;如果原來的牛是面向前方的(F),則被轉成了面向後方,需要再次反轉過來,f[i] = 1。

在計算過程中,我們只要一個值來記錄當前牛之前的K-1頭牛的f[j]之和,就可以算出當前牛的f[i]
代碼如下:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>

#define sf scanf
#define pf printf

using namespace std;

const int Maxn = 5010;
int f[Maxn],n;//f[i],以i開頭,長度爲k的區間是否需要反轉,需要f爲1,否則爲0
char s[Maxn];
void solved()
{
    int num = n,len = 1;
    for(int k = 1;k <= n;k ++)
    {
        int flag = 0,cnt = 0,sum =0;
        for(int i = 0;i < n;i ++)
        {
            if(i >= k) sum -= f[i - k];//記錄當前位置前k-1個值總共
            if(sum & 1)
                f[i] = (s[i] == 'F')?1:0;
            else
                f[i] = (s[i] == 'F')?0:1;
            sum += f[i];
            if(f[i] == 1)
            {
                if(i + k > n)//反轉區間長度<k,則不符合條件
                {
                    flag = 1;
                    break;
                }
                cnt ++;//反轉區間個數統計
            }
        }
        if(flag == 0)
        {
            if(cnt < num)
                num = cnt,len = k;
        }
    }
    pf("%d %d\n",len,num);

}
int main()
{
    while(~sf("%d",&n))
    {
        for(int i = 0;i < n;i ++)
            getchar(),sf("%c",&s[i]);
        solved();
    }
    return 0;
}

POJ 3279 Fliptile
農夫約翰知道聰明的牛產奶多。於是爲了提高牛的智商他準備瞭如下游戲。有一個M×N 的格子,每個格子可以翻轉正反面,它們一面是黑色,另一面是白色。黑色的格子翻轉後就是白色,白色的格子翻轉過來則是黑色。遊戲要做的就是把所有的格子都翻轉成白色。不過因爲牛蹄很大,所以每次翻轉一個格子時,與它上下左右相鄰接的格子也會被翻轉。因爲翻格子太麻煩了,所以牛都想通過儘可能少的次數把所有格子都翻成白色。現在給定了每個格子的顏色,請求出用最小步數完成時每個格子翻轉的次數。最小步數的解有多個時,輸出字典序最小的一組。解不存在的話,則輸出IMPOSSIBLE。
已知:
1 M,N 15

sample input
M = 4
N = 4 每個格子的顏色如下:(0表示白色,1表示黑色)
1 0 0 1
0 1 1 0
0 1 1 0
1 0 0 1
sample output
0 0 0 0
1 0 0 1
1 0 0 1
0 0 0 0


這裏寫圖片描述

如果繼續按照上面那題的思路,會發現行不通,因爲(1,1)反轉時會同時反轉(1,2)和(2,1),但是當(1,2)反轉時(1,1)又會受到影響,再次反轉。

於是不妨先指定好最上面一行的反轉方法,此時能夠反轉(1,1)的只剩下(2,1)了,所以可以直接判斷(2,1)是否需要反轉,類似的(2,1)~(2,N)都能這樣判斷,如此反覆,如果最後一行並非全白色,則意味着不存在可行的操作方法。

這樣算法的複雜度爲O(MN2N ) ,先對第一排進行0~(1<< N )- 1的數值枚舉,1代表反轉,0代表不反轉,然後依次根據上一行的數據判斷當前行有哪些需要反轉。

代碼如下:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <string>

#define sf scanf
#define pf printf

using namespace std;

const int Maxn = 20;
int n,m;
int a[Maxn][Maxn],b[Maxn][Maxn],ans[Maxn][Maxn],tmp[Maxn][Maxn];
int Find[20] = {0,1,0,-1,0,1,0,-1,0,0};
void solved()
{
    int len = (1 << m) - 1;
    int flag = 0,Min = n * m + 1;
    for(int k = 0;k <= len;k ++)//枚舉第一行的反轉情況
    {
        memcpy(b,a,sizeof(a));
        int val = k,step = 0;
        for(int j = 0;j < m;j ++)
        {
            if(val & 1)
            {
                for(int z = 0;z < 5;z ++)
                {
                    if(Find[z] <0 || Find[z] >=n || j + Find[z + 5] < 0 || j + Find[z + 5] >= m) continue;
                    b[Find[z]][j + Find[z + 5]] ^= 1;
                }
                step ++;
            }
            tmp[0][j] = val & 1;
            val >>= 1;
        }
        for(int i = 1;i < n;i ++)//第二行開始,根據前一行的信息判斷當前點是否需要反轉
            for(int j = 0;j < m;j ++)
            {
                if(b[i - 1][j] == 1)
                {
                    for(int z = 0;z < 5;z ++)//一個點反轉同時影響周圍4個點。
                    {
                        if(i  + Find[z] <0 || i + Find[z] >=n || j + Find[z + 5] < 0 || j + Find[z + 5] >= m) continue;
                        b[i + Find[z]][j + Find[z + 5]] ^= 1;
                    }
                    tmp[i][j] = 1;
                    step ++;
                }
                else
                    tmp[i][j] = 0;
            }

        int sum = 0;
        for(int j = 0;j < m;j ++)
            sum += b[n - 1][j];
        if(sum == 0)
        {
            if(step < Min)//尋找步數小的解
            {
                memcpy(ans,tmp,sizeof(tmp));
                Min = step;
            }
            flag = 1;
        }
    }
    if(flag == 0)
        pf("IMPOSSIBLE\n");
    else
    {
        for(int i = 0;i < n;i ++)
        {
            for(int j = 0;j < m - 1;j ++)
                pf("%d ",ans[i][j]);
            pf("%d\n",ans[i][m - 1]);
        }
    }
}
int main()
{
    while(~sf("%d%d",&n,&m))
    {
        for(int i = 0;i < n;i ++)
            for(int j = 0;j < m;j ++)
                sf("%d",&a[i][j]);
        solved();
    }
    return 0;
}

ps:poj上的這個題數據可能有點問題,題目雖然說了如果存在多個解就按輸出字典序最小的那個,但是只要在枚舉過程中找到一個就輸出然後return也是可以的,並不涉及多個解的情況,不知道是不是因爲從0開始枚舉,遇到的第一個答案就是步數最少的解,沒驗證過。。。

這題的另外一個做法是高斯消元(高斯消元可以查看之前的一篇文章

代碼如下:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <string>

#define sf scanf
#define pf printf

using namespace std;

const int Maxn = 20;

int a[Maxn][Maxn];
int paint[Maxn * Maxn][Maxn * Maxn],ans[Maxn * Maxn];
int Find[20] = {0,1,0,-1,0,1,0,-1,0,0};
void Guass(int n,int m)
{
    int i,row,col;
    for(row = 0,col = 0; row < n && col < n; row ++,col ++)
    {
        for(i = row;i < n;i ++)
            if(paint[i][col] == 1) break;
        if(i == n)
        {
            row --;
            continue;
        }
        if(i != row)
        {
            for(int j = 0;j < n + 1;j ++)
                swap(paint[row][j],paint[i][j]);
        }
        for(i = row + 1;i < n;i ++)
            if(paint[i][col])
            {
                for(int j = col;j < n + 1;j ++)
                    paint[i][j] ^= paint[row][j];
            }

    }
    for(i = row;i < n;i ++)
        if(paint[i][n] != 0)
        {
            pf("IMPOSSIBLE\n");
            return;
        }
    int num = 1 << (n - row),cnt = 0,Min = n + 1;
    for(i = 0;i < num;i ++)
    {
        cnt = 0;
        for(int j = n - 1,pos = i;j >= row;j--,pos >>= 1)
        {
            paint[j][j] = pos & 1;
            if(paint[j][j]) cnt ++;
        }
        for(int j = row - 1;j >= 0;j --)
        {
            int tmp = 0;
            for(int k = j + 1;k < n;k ++)
            {
                if(paint[j][k] == 0) continue;
                tmp ^= paint[k][k];
            }
            paint[j][j] = paint[j][n]^tmp;
            if(paint[j][j]) cnt ++;
        }
        if(cnt < Min)
        {
            Min = cnt;
            for(int j = 0;j < n;j ++)
                ans[j] = paint[j][j];
        }
    }
    for(int i = 0;i < n;i ++)
    {
        if(i % m == 0)
            pf("%d",ans[i]);
        else
            pf(" %d",ans[i]);
        if((i + 1) % m == 0)
            pf("\n");
    }
}
int main()
{
    int n,m;
    while(~sf("%d%d",&n,&m))
    {
        memset(paint,0,sizeof(paint));
        for(int i = 0;i < n;i ++)
            for(int j = 0;j < m;j ++)
            {
                sf("%d",&a[i][j]);
                for(int k = 0;k < 5;k ++)
                {
                    int x = i + Find[k];
                    int y = j + Find[k + 5];
                    if(x < 0 || x >= n || y < 0 || y >= m) continue;
                    paint[i * m + j][x * m + y] = 1;
                }
                paint[i * m + j][n * m] = a[i][j];
            }

        Guass(n * m,m);

    }
    return 0;
}
/*
3 3
0 1 0
1 0 1
0 1 0
*/

(3) 彈性碰撞


POJ 3684 Physics Experiment
用N個半徑爲R釐米的球進行如下實驗。
在H米高的位置設置一個圓筒,將求垂直放入(從下向上數第i個球的底端距離地面高度爲H + 2R)。實驗開始時最下面的球開始掉落,此後每一秒又有一個球開始掉落。不計空氣阻力,並假設球與球或地面間的碰撞時彈性碰撞。
請求出實驗開始後T秒時每個球底端的高度。假設重力加速度爲g=10m/s2
已知:
1 N 100
1 H 10000
1 R 100
1 T 10000

sample input
N = 1
H = 10
R = 10
T = 100

sample output
4.95


這裏寫圖片描述

看到這題我就想到了Ants這道題

從高位H的位置下落的話需要花費的時間:
H=12gt2
所以,t=2Hg

因此,在時刻T時,令K 爲滿足kt T的最大整數,那麼

H12g(Tkt)2(k)H12g(t(Tkt))2(k)

當R = 0時,如果認爲球是一樣的,就可以忽視他們的碰撞,視爲直接互相穿過繼續運動。由於在有碰撞時球的順序不會發生改變,所以忽略碰撞,將計算得到的座標進行排序後,就能知道每個球的最終位置。
那麼,R>0是要怎麼樣?這種情況下的處理方法基本相同,對於下方開始的第i個球,在按照R = 0計算的結果上加上2*R*i就可以了

代碼如下:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <string>

#define sf scanf
#define pf printf

using namespace std;

const int Maxn = 110;
double ans[Maxn];
int main()
{
    int cas,N,H,R,T,g = 10;
    sf("%d",&cas);
    while(cas--)
    {
        sf("%d%d%d%d",&N,&H,&R,&T);
        for(int i = 0;i < N;i ++)
        {
            double t = sqrt(2.0 * H / g);
            int k = floor(T / t);
            if(k < 0)
                ans[i] = H;
            else
            {
                if(k & 1)
                    ans[i] = H - 0.5 * g * (t - (T - k * t)) * (t - (T - k * t));
                else
                    ans[i] = H - 0.5 * g * (T - k * t) * (T - k * t);

            }
            T --;

        }
        sort(ans,ans + N);
        for(int i = 0;i < N;i ++)
            pf("%.2lf%c",ans[i] + 2.0 * R * i / 100,i + 1 == N ? '\n':' ');
    }
    return 0;
}
/*
2
1 10 10 100
2 10 10 100
*/

(4)折半枚舉(雙向搜索)


POJ 2785 4 Values whose Sum is 0
給定各有n個整數的四個數列A,B,C,D。要從每個數列中各取出1個數,使得四個數的和爲0,這出這樣的組合的個數。當一個數列中有多個相同的數字時,把它們作爲不同的數字看待。
已知:
1 n 4000
|(數字的值)|228

sample input
n = 6
A = {-45,-41,-36,-36,26,-32}
B = {22,-27,53,30,-38,-54}
C = {42,56,-37,-75,-10,-6}
D = {-16,30,77,-46,62,45}
sample output
5


如果全部枚舉,則有n4 種可能性。時間複雜度通不過。因此可以進行折半枚舉,計算A,B之間的組合,共有n2 種情況,同樣的,C,D之間也有n2 種情況。
在取出A,B組合中的一組組合(a + b)時,爲了使和爲0,去查找C,D組合中滿足a + b+c+d = 0的組合(c+d),這個查找可以用二分查找來實現,因此最後的複雜度是O(n2logn )

代碼如下:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <string>

#define sf scanf
#define pf printf

using namespace std;

typedef long long LL;
const int Maxn = 4010;
int A[Maxn],B[Maxn],C[Maxn],D[Maxn];
int AB[Maxn * Maxn],CD[Maxn * Maxn];
int n;
LL solved(int val)
{
    int l = 0, r = n * n - 1;
    LL ans;
    while(l <= r)
    {
        int mid = (l + r) / 2;
        if(CD[mid] <= val)
            l = mid + 1;
        else
            r = mid - 1;
    }
    ans = r;

    l = 0, r = n * n - 1;
    while(l <= r)
    {
        int mid = (l + r) / 2;
        if(CD[mid] < val)
            l = mid + 1;
        else
            r = mid - 1;
    }
    ans = ans - r;
    return ans;
}
int main()
{

    while(~sf("%d",&n))
    {
        for(int i = 0;i < n;i++)
            sf("%d%d%d%d",&A[i],&B[i],&C[i],&D[i]);
        int cnt = 0;
        for(int i = 0;i < n;i ++)
            for(int j = 0;j < n;j ++)
                AB[cnt] = A[i] + B[j],CD[cnt++] = C[i] + D[j];
        sort(CD,CD + cnt);
        LL ans = 0;
        for(int i = 0;i < cnt;i ++)
            ans += solved(0 - AB[i]);
        pf("%lld\n",ans);
    }
    return 0;
}
/*
3
0 0 0 0
0 0 0 0
0 0 0 0
*/

超大揹包問題:
有重量和價值分別爲wi,vi 的n個物品。從這些物品中挑選總重量不超過W的物品,求所有挑選方案中價值總和的最大值。
已知:
1 n 40
1wi,vi1015
1W1015

sample input
n = 4
w = {2 , 1 , 3 , 2}
v = {3 , 2 , 4 , 2}
W = 5
sample output
7(挑選0,1,3號物品)

如果這個題用揹包問題來做,W太大,內存不夠,但是這題中n的範圍很小。因此可以用枚舉來做。
挑選物品的方法共有2n 種,所以不能直接枚舉,可以考慮使用折半枚舉,220 是可以接受的。前半部分選取對應的重量和價值總和記爲w1,v1。這樣在後半部分尋找總重w2Ww1 時使v2最大的選取方法就好了。
因此,主要思考從枚舉得到的(w2,v2)的集合中高效尋找max{v2|w2W} 的方法。首先排除w2[i]w2[j]v2[i]v2[j] 中的j, 此後剩餘的元素都滿足w2[i]< w2[j] , v2[i]< v2[j] ,可使用二分搜索進行查找。算法總複雜度爲O(2(n/2)n )。

(5) 座標離散化


區域的個數
w*h的格子上畫了n條或垂直或水平的寬度爲1的直線,求出這些線將格子劃分成了多少個區域。
這裏寫圖片描述
已知:
1w,h1000000
1n500

sample input
w = 10,h = 10,n = 5
x1 = {1 , 1 , 4 , 9 , 10}
y1 = {4 , 8 , 1 , 1 , 6}
x2 = {6 , 10 , 4 , 9 , 10}
y2 = {4 , 8 , 10 , 5 , 10}
(對應上圖,橫向爲x,縱向爲y)


利用BFS或dfs可以求出被分割的區域,但是w,h太大,不能創建w*h的數組,所以需要用到“座標離散化” 這一技巧。
如下圖:
這裏寫圖片描述

將前後左右沒有變化的行列消除後並不會影響區域的個數。數組裏重要存儲有直線的行列以及其前後的行列就足夠了。這樣的話最多6n*6n就足夠了,因此可以創建出數組並利用搜索求出區域的個數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章