哈爾濱理工大學3-31校賽模擬賽第一場題解

BIOSOIB發表於2024-04-01

概覽:ABF為簽到題,CE模擬,D深搜,G最短路,H雙指標

A.提取數字:
注意前導零的情況需要排除,由於組成的數不超過long long範圍,所以直接用res承接就好了

#include<iostream>
using namespace std;
long long res;
int main()
{
    char c;
    while(cin>>c)
            if(c>='0' && c<='9')
                    res=res*10+(c-'0');
    cout<<res<<endl;
}

B.保齡球:
map/unordered_map的應用題。

#include<iostream>
#include<unordered_map>
using namespace std;
unordered_map<int,int>ac;
int n,x;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
            cin>>x,ac[x]=i;
    cin>>n;
    while(n--)
            cin>>x,cout<<ac[x]<<"\n";
}

C.乒乓球:
模擬,見下圖解釋

#include <iostream>
using namespace std;
int tot[2], now[2], k, k0, tmp[2], mark;//tot代表總比分,now代表當前局數比分,tmp代表兩位發球手各自的連續發球數,k0表示當前大局誰發球,k表示當前小回合誰發的球
char c;                    //0代表‘L’相關操作,1代表‘Z’相關操作
string s;
void oper()//用於一大局結束時的計分與重置
{
    k0 ^= 1, k = k0;//當一個數只可能為0或1時,其異或後就是對方,模擬大局結束,發球手對換的情況,同理k=k0要保持發球手的一致
    if (now[0] > now[1])
        tot[0]++;
    else
        tot[1]++;
    now[1] = tmp[1] = now[0] = tmp[0] = 0;//全部重置
}
int main()
{
    cin >> c, k0 = k = (c == 'Z' ? 1 : 0), cin >> s;
    for (int i = 0; i < s.size(); i++)
    {
        c = s[i], tmp[k]++, c == 'L' ? now[0]++ : now[1]++;//統計當前發球手的連續發球次數,以及小分增加
        if (now[0] == 10 && now[1] == 10)
            mark ^= 1;//mark代表當前進入10:10以後的特殊模式,對應了不同的發球邏輯和勝利邏輯
        if (mark)
        {
            tmp[k] = 0, k ^= 1;//發球是輪流的,所以在mark模式下,每次發球都清空tmp次數,並且交換髮球權
            if (abs(now[0] - now[1]) > 1)
                mark ^= 1, oper();
        }
        else
        {
            if (tmp[k] > 1)//只有連續發兩個及以上的時候才清空tmp,交換髮球權
                tmp[k] = 0, k ^= 1;
            if (now[0] == 11 || now[1] == 11)
                oper();
        }
    }
    if (tot[1] == 4 || tot[0] == 4)
        cout << "E" << endl;//大場有人勝利輸出E
    else
        cout << (k ? 'Z' : 'L') << endl;
}

D.beam search演算法:
閱讀理解題,題面比較長,但是耐心讀到後面發現是一個思路比較清晰的DFS,由於相乘的都是小數,n又最多為一百個,肯定會有精度問題,所以這裡用cmath庫裡的log函式把小數乘轉為對數加,然後再用exp把對數化回小數。

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
#define double long double
const int N = 105;
int n, m, x;
vector<int> h[N][N];//存鄰層之間的連邊關係
double p[N][N], res = 0.0;
void dfs(int i, int j, double t)//這個dfs用來求最大的小數乘積
{
if (i == n) return res = max(res, exp(t)), void(); for (int k = 0; k < h[i][j].size(); k++) dfs(i + 1, h[i][j][k], t + p[i + 1][h[i][j][k]]);//對數加等價於小數乘 } void get_ans(int i, int j, double t, string path)//求字典序最小的路徑,由於我們列舉的時候從小列舉到大,所以得到的同值路徑一定是字典序最小的 { if (i == n) { if (fabs(exp(t) - res) < 1e-60)//這個精度很離譜,但是這種做法如果不開到1e-60回因為精度WA掉個別測試資料 cout << path << endl, exit(0); return; } for (int k = 0; k < h[i][j].size(); k++) { int u = h[i][j][k]; get_ans(i + 1, u, t + p[i + 1][u], path + to_string(u)); } } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> p[i][j], p[i][j] = log(p[i][j]); for (int i = 2; i <= n; i++) for (int j = 1; j <= m; j++) cin >> x, h[i - 1][x].push_back(j);//邊從上一層連線到這一層 for (int i = 1; i <= m; i++) dfs(1, i, p[1][i]); for (int i = 1; i <= m; i++) get_ans(1, i, p[1][i], to_string(i)); }

E.50公里徒步

一個需要注意個別細節處理的模擬。(大量壓行+註釋,可能會影響觀感)

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 105;
int n, h, m, x, dist[N], tmp[N], mark[9], lim[N], t[N], T;//dist表示總步數,tmp表示從入場到給出的時間截點選手走了多少步,lim表示選手最多路程是多少
int d[8] = {0, 5, 7, 4, 5, 4, 7, 8}, a[N], st[N];//t表示選手從入場到時間截點一共有多少時間,mark表示獲取i個印章至少需要mark[i]的時間
int main()
{
    for (int i = 1; i <= 7; i++)
        mark[i] = mark[i - 1] + d[i] * 1000;
    cin >> n, mark[8] = 0x3f3f3f3f;//最後一個結點設定一個正無窮哨兵,可以省去特判
    for (int i = 1; i <= n; i++)
        cin >> dist[i];//由於初始微信步數也會算在最後排榜時的步數,所幸直接加到dist中
    for (int i = 1; i <= n; i++)//這裡雖然題目說的是長度為四的字串,但由於時間都是嚴格四位的,所以直接用int輸入就可以了,前導零也會自動省略
        cin >> x, h = x / 100, m = x % 100, t[i] = h * 60 + m - 480;
    for (int i = 1; i <= n; i++)//全程長度為40000米,加上選手起始步數和回家步數,就是選手這次比賽可能達到的最大的步數
        cin >> lim[i], lim[i] += dist[i] + 40000;
    cin >> x, h = x / 100, m = x % 100, T = h * 60 + m;//大T表示時間截點
    for (int i = 1; i <= n; i++)
    {
        tmp[i] = max(0, (T - t[i]) * 60), dist[i] += tmp[i], dist[i] = min(dist[i], lim[i]);//tmp和0取max是怕選手入場時間晚於時間截點出現負步的情況
        for (int j = 0, flag = false; !flag && j <= 8; j++)//為了壓行不寫花括號,引入flag變數,相當於break的作用了
            if (tmp[i] < mark[j])
                cout << j - 1 << " ", flag = true;
    }
    cout << endl, memcpy(a, dist, sizeof(a)), sort(a + 1, a + 1 + n, greater<int>());//這裡直接sort然後reverse印象中會快一點,不必按照新的比較規則排序
    for (int i = 1; i <= n; i++)
    {
        int rk = 1;
        while (a[rk] > dist[i] || (a[rk] == dist[i] && st[rk]))//st是為了標記排名,因為步數可以重但是排名不會重,所以對於同步數也要分配不同且遞增的排名
            rk++;
        cout << rk << " ", st[rk] = true;
    }
    cout << endl;
}

F.作業抽查

搞個字首和陣列,相同就標記為1,每次詢問判一下區間內是否權值和等於區間長度即可.

(注:由於資料太水,放掉了對兩個陣列求字首和然後判斷區間權值和是否相等的做法,這是很錯誤的解法)

#include <iostream>
using namespace std;
const int N = 1e5 + 5;
int a[N], b[N], s[N], n, m, l, r;
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 1; i <= n; i++)
        cin >> b[i];
    for (int i = 1; i <= n; i++)
        s[i] = s[i - 1] + (a[i] == b[i]);
    cin >> m;
    while (m--)
        cin >> l >> r, cout << ((s[r] - s[l - 1] == r - l + 1) ? "Yes" : "No") << "\n";
}

G.馬和馬車 (0個提交的原因難道是我題面寫的比較模糊?自己看的時候發現其實有些歧義可能會讓人把題想難)

思路其實並不難想,但是程式碼相對長一些,需要注意一些細節.(另外,邊權為1的最短路其實也可以BFS求)

二者的樸素走法:
馬:dx和dy陣列進行八個方位的列舉即可

馬車:由於馬車橫著走,改變一單位x,豎著走,改變一單位y,斜著走,同時改變x和y各一個單位,所以馬車想去一個點所需的路程等價於起點和終點的x與y之差的最大值(因為斜著走可以同時改變x和y,所以x和y中較小的一方是可以斜著順帶走掉的)

怎麼找最少步數:

由於馬車只有一個,但是馬有很多,而且馬車的走法是可以O(1)求得的,所以我們容易想到先算出所有馬的最步數總和,然後再討論馬車如何走的問題.

既然只討論馬的走法,那麼我們對於棋盤的建圖也要依據馬的八個方向走法進行連線.

求多個點到一個點的最短距離和,一個常用的方法是單源最短路,從匯聚點出發,得到到達各個點的最短距離,然後把各個點的dist值加起來得到總和本題,由於R*C很小,所以我們考慮列舉各個點作為匯聚點來求所有馬到匯聚點的最少步數和.

再討論,在考慮馬車的情況下如何求得最少的步數和.

馬車有兩種走法,由於自己是八連通的走法,所以馬車到達任何點都可以靠自己的max(x距離差,y距離差)走到,也可以考慮找一匹馬搭順風車,這樣由於二者結合以後只按照馬的方式走,所以我們可以認為馬車到匯聚點所花費的步數等於到達"搭車點"的步數,然後就是被搭車馬帶飛,按照它的最短路走即可.所以問題的關鍵是如何找到"搭車點".

不妨繼續列舉搭車點,但一個顯然的問題是,搭車點不一定在某個馬的最短路徑上,存在"馬繞遠去接馬車然後再跳到終點的步數少於馬和馬車各走各的最短路徑的步數和",所以如果我們能直到,馬車在棋盤上任意兩點之間的最短距離就可以快速算出搭車情況下的最短步數和.所以我們開二維dist做n*m次最短路得到不同點之間馬的最短路.

這樣,列舉匯聚點以後,對於搭車和不搭車我們都可以有直接的公式算出其最少步數和,然後對res取min即可,其計算公式如下:
要麼不搭車,此時總步數和為匯聚點到所有馬的初始點的dist總和+馬車與匯聚點的橫縱座標之差中的最大值

要麼搭車,此時列舉搭車點,然後列舉所有馬的起點,此時對於搭車點b,馬起點a,匯聚點c三個點而言,馬車搭車點最少步數之和為"匯聚點到所有馬起點的步數和減去到起點a的步數和,加上起點a到搭車點b的步數,加上搭車點b到匯聚點c的步數,加上馬車到搭車點b的步數",在這個式子中找到最小值,得到答案.

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int>PII;
const int N=1e3+5,M=1e6+5;
int idx,id[30][26],cnt,dist[N][N],xx,yy,h[N],e[M],ne[M],w[M],n,m;
int dx[8]={-1,-2,-2,-1,1,2,2,1},sx,sy;
int dy[8]={-2,-1,1,2,2,1,-1,-2},id1,id2;
bool st[N];
vector<int>dot;
void add(int a,int b)
{
    e[idx]=b,w[idx]=1,ne[idx]=h[a],h[a]=idx++;
}
void dj(int sx,int dist[])//堆最佳化迪傑斯特拉
{
    priority_queue<PII,vector<PII>,greater<PII>>q;
    memset(st,false,sizeof(st)),dist[sx]=0,q.push({dist[sx],sx});
    while(q.size())
    {
        PII t=q.top();
        q.pop();
        int ver=t.second;
        if(st[ver])
            continue;
        st[ver]=true;
        int distance=t.first;
        for(int i=h[ver];~i;i=ne[i])
        {
            int j=e[i];
            if(dist[j]>distance+w[i])
                dist[j]=distance+w[i],q.push({dist[j],j});
        }
    }
}
int get_dist(int x1,int y1,int x2,int y2)//馬車的樸素走法所需最少步數
{
    return max(abs(x1-x2),abs(y1-y2));
}
int check(int x,int y)//對於匯聚點{x,y}而言,求考慮馬車的情況下的最小步數和
{
    int res=get_dist(x,y,sx,sy);//初始化就定為馬車樸素走法的距離,可以省略以後再次判斷.
    for(int i=0;i<n;i++)//列舉搭車點
        for(int j=0;j<m;j++)
        {
            int d=get_dist(i,j,sx,sy);
            if(res > d)//小剪枝
            {
                int a=id[x][y],b=id[i][j];
                for(int k=0;k<dot.size();k++)
                {
                    int c=dot[k];
                    res=min(res,d+dist[a][b]+dist[b][c]-dist[a][c]);//形似三角形的兩邊替換一邊
                }
            }
        }
    return res;
}
int main()
{
    char y;
    int x;
    memset(h,-1,sizeof(h)),memset(dist,0x3f,sizeof(dist));
    cin>>n>>m>>y>>x,sx=x-1,sy=y-'A';
    for(int i=0;i<n;i++)
        for(int j=0;j<m;j++)
            id[i][j]=i*m+j;
    while(cin>>y>>x)
    {
        int i=x-1,j=y-'A';
        dot.push_back(id[i][j]);
    }
    for(int i=0;i<n;i++)//按馬走法連邊
        for(int j=0;j<m;j++)
            for(int k=0;k<8;k++)
            {
                xx=i+dx[k],yy=j+dy[k];
                if(xx<0||xx>=n||yy<0||yy>=m)
                    continue;
                id1=id[i][j],id2=id[xx][yy],add(id1,id2);
            }
    int res=0x3f3f3f3f;
    for(int x=0;x<n;x++)
        for(int y=0;y<m;y++)
            dj(id[x][y],dist[id[x][y]]);//求得dist陣列
    for(int x=0;x<n;x++)//列舉匯聚點
        for(int y=0;y<m;y++)
        {
            int tot=0;
            for(int j=0;j<dot.size();j++)
                tot+=dist[id[x][y]][dot[j]];
            if(tot<0)
                continue;
            res=min(res,tot+check(x,y));
        }
    if(res>1e9)//說明有的馬車點走不到
            cout<<"What can I say\n";
    else
        cout<<res<<endl;
}

H.小明刷USACO

題意簡化:把一段陣列劃分三個部分,使得三個部分權值和相等,並且非空,問這種劃分方式有多少種

首先,想劃分三個等值非空陣列的前提是整個陣列可以被三整除,這樣可以排除一些情況,還有可能陣列容量不足3,但這種情況在我們的做法中是不必特判的,因為雙指標找不到合適的位置安置.

然後考慮如何劃分,容易想到字首和快速判斷區間權值和,我們用一個指標r從後往前劃分,那麼一旦劃分出右側元素和為總數的三分之一,則啟用指標l從前往後劃分,當該指標左側元素和也為總數的三分之一時,二者指標中間的元素和自然也為三分之一,所以我們要做的就是找到這種{l,r}對.

但是對於極限資料,n等於十萬,全是0的情況,雙指標會超時(甚至種類數會爆long long),我們需要想到一種快速的計數方法:把l和r指標的成立位置記下來,升序排序,然後列舉左側指標的位置,同時尋找第一個大於該位置的右側指標(小於該位置的右側指標跳過),此時還有多少個右側指標未被跳過,就額外多有幾種組合方案,加上即可.

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int tot,k,s[N],a[N],n;
long long res;
vector<int>pl,pr;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i],s[i]=s[i-1]+a[i],tot+=a[i];
    if(tot%3)
        cout<<0<<endl,exit(0);
    k=tot/3;
    for(int i=1;i<n-1;i++)
        if(s[i]==k)
            pl.push_back(i);
    for(int i=3;i<=n;i++)
        if(s[n]-s[i-1]==k)
            pr.push_back(i);
    int l=0,r=0;
    while(l<pl.size())
    {
        while(r<pr.size() && pl[l]+1 >= pr[r])
            r++;
        if(r>=pr.size())
            break;
        res+=pr.size()-r,l++;
    }
    cout<<res<<endl;
}

相關文章