2019 西電ACM校賽網絡賽 題解


    今年題目難度有較大提升,總體與往年類似,數學題居多。以下為我通過的部分題解。

    賽題鏈接:http://acm.xidian.edu.cn/contest.php?cid=1053

A - 上帝視角

我也沒去過澳門賭場,不熟悉什么籌碼之類。看完題有點懵,但畢竟是簽到題。

題目大概是隱含了總籌碼數量相同這一條件,然后每個人開始的籌碼都是一樣的。給你一組每個人手上籌碼的局面,然后有q組詢問,讓你判斷現在局面是否合法,其中一個人贏了還是輸了。

比較簡單,廢話不多說直接上代碼:

#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;

ll arr[1010], qi[1010];
int main()
{
    int n, m, q;
    ll total = 0;
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>arr[i], total += arr[i];
    cin>>q;

    for(int i=0;i<q;i++)
        cin>>qi[i];

    if(total%n)
        cout<<"you ren chu qian?\n";
    else {
        total /= n;
        for(int i=0;i<q;i++)
            if(arr[qi[i]-1]>total) cout<<"jian hao jiu shou!\n";
            else if(arr[qi[i]-1]<total) cout<<"ji shi zhi sun!\n";
            else cout<<"wei shi bu wan!\n";
    }
    return 0;
}
View Code

 

B - Shocking! Two Acmer Doing This In The Lab!

感覺太復雜了,一直沒做,AC了再來更新吧。大概就是一個思維題。

 

C - XY之說走就走的旅行

簡單的BFS,開始題目看錯了,以為求min(disA+disB),交上去WA了一發,再看題發現是求min(max(disA, disB));

因為偷懶用一個數組存(i,j)到兩地的最大距離,不明不白WA了半天。。。思考問題一定要考慮周全啊!!!

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;

int n, m;
char mp[220][220];
int s1x, s1y, s2x, s2y, k;
bool vis[220][220];
int dis[220][220];
const int dx[]={0, 0, 1, -1};
const int dy[]={1, -1, 0, 0};
struct node{
    int x, y;
    int step;
    node(int xx=0, int yy=0, int s=0):x(xx), y(yy), step(s){}
} sta[220];
void bfs(int t, int x, int y)
{
    vis[x][y] = 1;
    queue<node> q;
    q.push(node(x, y, 0));
    while(q.size()) {
        node now = q.front(); q.pop();
        for(int i=0;i<4;i++) {
            int nx = now.x+dx[i];
            int ny = now.y+dy[i];
            if(nx>=0 && nx<n && ny>=0 && ny<m && !vis[nx][ny] && mp[nx][ny]!='#') {
                vis[nx][ny] = 1;
                if(mp[nx][ny]=='P') {
                    if(t==0) dis[nx][ny] = now.step+1;
                    else if(dis[nx][ny])
                        dis[nx][ny] = max(now.step+1, dis[nx][ny]);
                }
                q.push(node(nx, ny, now.step+1));
            }

        }
    }
}
int main()
{
    while(cin>>n>>m)
    {
        k = 0;
        getchar();
        for(int i=0;i<n;i++) {
            scanf("%s", mp[i]);
            for(int j=0;mp[i][j];j++)
                if(mp[i][j]=='P') sta[k].x = i, sta[k++].y=j;
                else if(mp[i][j]=='X') s1x = i, s1y = j;
                else if(mp[i][j]=='Y') s2x = i, s2y = j;
        }
        memset(dis, 0, sizeof(dis));
        memset(vis, 0, sizeof(vis));
        bfs(0, s1x, s1y);
        memset(vis, 0, sizeof(vis));
        bfs(1, s2x, s2y);

        int x, ans = 10000000;
        for(int i=0;i<k;i++)
            if(dis[sta[i].x][sta[i].y] && dis[sta[i].x][sta[i].y]<ans)
                x = i, ans = dis[sta[x].x][sta[x].y];
        cout<<sta[x].x+1<<' '<<sta[x].y+1<<endl;
    }

    return 0;
}
View Code

 

D - 武舉考試安排表

這題難點在於讀懂題目,然后就是要打表找規律。直白說比賽安排表類似一個數獨,添上一行123...N的表頭代表隊員編號的話,那么整個表就有N行N列。我們要使每行每列都有1~N,並且要找到一個字典序最小的方案。

第一天我很天真地以為,簡單把123...N的排列每次循環左移就能得到字典序最小的安排表。WA了三次后,在紙上排一遍發現還能有更優的排法,排表過程中要用到dfs,復雜度O(22N),就扔到一邊沒管了。

過了兩天根據Wu找的規律幾分鍾寫了代碼就直接AC了,哈哈哈。預處理n=10的安排表,查詢O(1),總的時間復雜度O(220+T)。

#include<iostream>
#include<cstdio>
using namespace std;
int ans[1024][1024];
const int num[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048};
void pre()
{
    ans[0][0] = ans[1][1] = 1;
    ans[0][1] = ans[1][0] = 2;

    int s = 2;
    while(s<1024) {
        for(int i=s;i<2*s;i++)
        for(int j=0;j<s;j++)
            ans[i][j] = s+ans[i-s][j];

        for(int i=0;i<s;i++)
        for(int j=s;j<2*s;j++)
            ans[i][j] = s+ans[i][j-s];

        for(int i=s;i<2*s;i++)
        for(int j=s;j<2*s;j++)
            ans[i][j] = ans[i-s][j-s];
        s *= 2;
    }
}
int main()
{
    pre();
    for(int i=0;i<32;i++) {
        for(int j=0;j<32;j++)
            printf("%2d ", ans[i][j]);
        cout<<endl;
    }
    int T; cin>>T;
    int n, m, x;
    while(T--)
    {
        scanf("%d %d %d", &n, &m, &x);
        if(m<1||m>num[n]||x<1||x>num[n]-1)
            cout<<"Wrong Query!\n";
        else {
            cout<<ans[x][m-1]<<endl;
        }

    }
    return 0;
}
View Code

 

E - 最后一個

一眼掃去這不就是Nim博弈嗎?然后就快速寫了個Nim和,交上去就WA了。再細看發現結束的規則不一樣,取走最后的石子者為敗家。想了半天還是不太會博弈,既然狀態有限,就寫了記憶化搜索,調試好樣例交上就AC了。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int s[7][7][7][7][7][7];  // s=0 敗  s=1 勝
int solve(int a, int b, int c, int d, int e, int f)
{
    if(a+b+c+d+e+f==1) return 0;
    if(s[a][b][c][d][e][f]!=-1)
        return s[a][b][c][d][e][f];

    for(int i=1;i<=a;i++)
        if(solve(a-i, b, c, d, e, f)==0) return s[a][b][c][d][e][f]=1;
    for(int i=1;i<=b;i++)
        if(solve(a, b-i, c, d, e, f)==0) return s[a][b][c][d][e][f]=1;
    for(int i=1;i<=c;i++)
        if(solve(a, b, c-i, d, e, f)==0) return s[a][b][c][d][e][f]=1;
    for(int i=1;i<=d;i++)
        if(solve(a, b, c, d-i, e, f)==0) return s[a][b][c][d][e][f]=1;
    for(int i=1;i<=e;i++)
        if(solve(a, b, c, d, e-i, f)==0) return s[a][b][c][d][e][f]=1;
    for(int i=1;i<=f;i++)
        if(solve(a, b, c, d, e, f-i)==0) return s[a][b][c][d][e][f]=1;

    return s[a][b][c][d][e][f]=0;

}
int arr[6];
int main()
{
    memset(s, -1, sizeof(s));
    s[0][0][0][0][0][0] = 1;
    int n;
    while(scanf("%d", &n)!=EOF) {
        memset(arr, 0, sizeof(arr));
        for(int i=0;i<n;i++)
            scanf("%d", &arr[i]);

        printf("%s\n", solve(arr[0],arr[1],arr[2],arr[3],arr[4],arr[5])?"orzwym6912":"orzwang9897");
    }
    return 0;
}
View Code

 

網上查閱了相關的題,理解了這個規則其實同樣可以用Nim和來解決,只需要特殊判斷全部堆都是1的情況。因為在Nim博弈中,最后一步要取走全部石子,那么本題對於必勝者也可以少取走一顆石子,那么也是必勝。但是如果每堆只有一顆石子,就無法按照Nim博弈的進行最優取法。顯然有偶數個為1的堆為必勝態,那么問題就解決了。

#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
    int n;
    while(scanf("%d", &n)!=EOF) {
       int ans = 0, k;
        bool flag = 1; int cnt = 0;
        while(n--) {
            scanf("%d", &k);
            if(k==0) continue;

            if(k!=1) flag = 0;
            else cnt++;
            ans ^= k;
        }
        if(!flag)
            printf("%s\n", ans==0?"orzwang9897":"orzwym6912");
        else
            printf("%s\n", cnt&1?"orzwang9897":"orzwym6912");
    }
    return 0;
}
View Code

 

F - 背包彈夾平底鍋

通過找規律+oeis.org歸納出公式,最后發現了這題主要是得到第二類斯特林數。結果就是s(n, i)*i!/n^m。

那么怎么求s(n, i)呢?查閱資料得知這個跟組合數有相似的遞推性質,可以在O(n2)時間內求出s(n, i)。此題n,m<100000,顯然不行。

翻了好多篇博客,都提到要用快速傅里葉變換,然后就自閉了。

(待補。。。)

 

G - 小鳥的修路計划

這題就是求有n個不同節點的連通圖的種類。

開始自己手算了遞推式,樣例都算不對,只好百度借鑒了別人的公式。

寫好快速冪+組合數WA了好多次,debug很久,一度懷疑冪運算取模出錯了,要用那啥費馬小定理。最后比對別人的輸出才突然注意到三個ll相乘會溢出的重大bug。。。

先預處理,T次查詢直接輸出結果,時間復雜度O(m2+T)。

#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;

const ll mod=1000000007;
ll C[1010][1010], f[1010];
ll pow_mod(ll a, ll x)
{
    ll res = 1;
    while(x) {
        if(x&1) res = (res*a)%mod;
        a = (a*a)%mod;
        x >>= 1;
    }
    return res;
}
ll Cn2(int n)
{
    return (ll)n*(n-1)/2;
}
void solve()
{
    for(int i=0;i<1010;i++)
    C[i][i] = C[i][0] = 1;

    for(int i=1;i<1010;i++)
    for(int j=1;j<=i;j++) {
        C[i][j] = (C[i-1][j] + C[i-1][j-1])%mod;
    }

    f[1] = f[2] = 1;
//    f[3] = 4;
    for(int n=3;n<1010;n++) {
        for(int i=1;i<n;i++)
            f[n] = (f[n] + ((C[n-1][i-1]*f[i])%mod * pow_mod(2, Cn2(n-i))%mod))%mod;

        f[n] = ((pow_mod(2, Cn2(n))-f[n])%mod+mod)%mod;
    }
}

int main()
{
    int T; cin>>T;
    solve();
    while(T--) {
        int m; scanf("%d", &m);
        printf("%lld\n", f[m]);
    }
    return 0;
}
View Code

// 2的C(n,2)次方我為什么要用組合數。。。

 

H - 超長遞增序列

一個簡單技巧題被我硬生生用二分法暴力求解,一直TLE到懷疑人生。(雖說復雜度O(T*2n*logn)很勉強的樣子)

由於a1 + a2 + ... + ai <= a(i+1),那么對於K,我們從后往前查找,如果a(i+1)<=K,不取a(i+1)的話就無法選擇前i項使總和為K,所以a(i+1)必選。因此不斷往前貪心即可。

時間復雜度O(T*n)

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long ll;
ll arr[44], K;

ll pow2(ll a, int x)
{
    ll res = 1;
    while(x) {
        if(x&1) res *= a;
        a *= a;
        x >>= 1;
    }
    return res;
}

int main()
{
    int T, n; scanf("%d", &T);
    while(T--) {
        scanf("%d %lld", &n, &K);
        for(int i=1;i<=n;i++)
            scanf("%lld", &arr[i]);

        int i=n;
        ll res = 0;
        while(K>0) {
            while(i>=1 && arr[i]>K) i--;
            if(i<1) break;
            else
                K -= arr[i], res += pow2(2, i);
        }
        if(K==0)
            printf("%lld\n", res);
        else
            printf("-1\n");
    }
    return 0;
}
View Code

 

 I - 兩數和

題意:給了n個數兩兩之間C(n,2)組和,求出原數列,如有多組,輸出字典序最小的解。

假設有a1<=a2<=a3<=... <=an,可以得到最小和一定是a1+a2,次小和一定是a1+a3。(仔細想想是不是)

那么如果我們假設第三小的和是a2+a3的話,前三個數就確定了。顯然接下來最小的和就是a1+a4,求出a4,刪去a2+a4,a3+a4,那么最小的就是a1+a5,依次類推,a5,a6,... ,都能計算出來。

但問題沒這么簡單。

我們其實無法確定a1+a4,a1+a5,..., a1+an與a2+a3的大小順序。可以肯定的是,a2+a3的值一定在這n-3+1=n-2組最小值之中。

所以采用窮舉並檢驗后續能否全部算出,時間復雜度O(T*n3),可行。

PS. 注意n=2的特殊處理。

// 考慮到每次檢驗失敗都要重新傳入n*(n-1)/2組值,感覺會拖慢時間,自作聰明將multiset<ll>傳引用,結果WA了半天還不知道哪出錯了。。。

// 對於要在函數內進行修改且不需要返回更新值的變量,還是不要想當然傳引用了。。。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<set>
#include<vector>
using namespace std;
typedef long long ll;
typedef multiset<ll>::iterator msPt;
int n;
ll ai, a[120];
bool solve(ll c1, ll c2, ll c3, multiset<ll> S)
{
    if((c1+c2+c3)%2)
        return 0;

    if(c1+c3<=c2||c1+c2<=c3)
        return 0;

    a[1] = (c1+c2-c3)/2;
    a[2] = (c1+c3-c2)/2;
    a[3] = (c2+c3-c1)/2;

    for(int i=4;i<=n;i++) {
        a[i] = *S.begin()-a[1]; S.erase(S.begin());

        for(int j=2;j<i;j++) {
            if(S.find(a[j]+a[i])==S.end())
                return 0;
            else
                S.erase(S.find(a[j]+a[i]));
        }
    }

    for(int i=1;i<=n;i++)
        printf("%lld%c", a[i], n==i?'\n':' ');
    return 1;
}


int main()
{
    int T; cin>>T;
    while(T--) {
        scanf("%d", &n);
        if(n==2) {
            scanf("%lld", &ai);
            if(ai>1)
                printf("1 %lld\n", ai-1);
            else
                printf("Impossible\n");

            continue;
        }

        multiset<ll> S;
        for(int i=0;i<n*(n-1)/2;i++) {
            scanf("%lld", &ai);
            S.insert(ai);
        }
        ll c1 = *S.begin(); S.erase(S.begin());
        ll c2 = *S.begin(); S.erase(S.begin());
    //    ll c3 = *S.begin(); S.erase(S.begin());

        vector<ll> C3;
        for(msPt it = S.begin(); C3.size()<n-2&& it!=S.end();it++)
            if(C3.size() && *it==C3[C3.size()-1]) continue;
            else
                C3.push_back(*it);

         bool flag = 0;
        for(int i=0;i<C3.size();i++) {
            S.erase(S.find(C3[i]));
            if(!solve(c1, c2, C3[i], S)) {
                   S.insert(C3[i]);
            } else {
                flag = 1;
            }
        }
        if(!flag)
            printf("Impossible\n");
        
    }
    return 0;
}
View Code

 

J - 壘箱子

哈哈不會,據說需要各種暴力+建圖。我還是好好掌握prim啊、dijkstra啊這種再來吧。。。

 

K - 簽到

到了傳說中的簽到題,好幾天都一直沒人做。讀完題發現就是求解三個球面的交點問題,可以直接聯立方程求解。

為了體現出它不是一個數學問題,我嘗試用計算幾何的思維,二分兩平面的交線。WA了十來次才發現連輸出要求都沒注意。改過后仍然WA。。。

最后還是用數學方法解方程通過的。賽后題解提供了五六種思路,我嘗試了兩種二分都失敗了,改日再研究計算幾何。。。

 View Code

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM