CSP歷年複賽題-P1043 [NOIP2003 普及組] 數字遊戲

江城伍月發表於2024-05-22

原題連結:https://www.luogu.com.cn/problem/P1043

題意解讀:將n個環形數分成任意m組,組內求和再%10、負數轉正,組間相乘,求所有分組方案中得到結果的最小值和最大值。

解題思路:

比賽題的首要目的是上分!此題一看就是DP,但是苦苦思索了半天,想不清楚狀態表示,那麼可以換換策略,先暴力得分再說!

暴力的思路:

1、對分組方案進行列舉,n個數分成m組,即將n拆解為m個數之和,用DFS搜尋所有的方案,存入陣列b[]

2、再從環形陣列任意位置開始,根據方案陣列b[],依次計算每個組內的和、再%10,各組的結果相乘,更新最大、最小值

3、為了簡化環形陣列的處理,可以將陣列a[n]複製2倍長成a[2n],從1~n任意位置開始,根據分組方案進行計算即可

4、對於每組資料求和,可以透過字首和來提速

很驚喜,可以得到80分(比賽中如果此題得到80分也不錯了:))

80分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int N = 55, M = 10;
int n, m;
int a[2 * N]; //原數字,擴充2倍長度
int s[2 * N]; //字首和
int b[M]; //一種分配方案:分成m組,每組幾個數
int maxans = INT_MIN;
int minans = INT_MAX;

//給第k組分數,一共還有cnt個數
void dfs(int k, int cnt)
{
    if(k == m) //如果是給最後一組分數
    {
        b[k] = cnt; //剩下的只能全分給最後一組
        //從1~n任意一個作為起點,按照分配方案把n個數共m組進行分別計算
        //每一組求和,%10,各個組相乘,記錄最大、最小值
        for(int i = 1; i <= n; i++)
        {
            int start = i; //每一段的起始位置
            int ans = 1;
            for(int j = 1; j <= m; j++)
            {
                int sum = s[start + b[j] - 1] - s[start - 1]; //利用字首和計算每一段的和
                start += b[j]; //start更新為下一段的起始位置
                sum = (sum % 10 + 10) % 10; //避免sum是負數,取模加10再取模
                ans *= sum;
            }
            maxans = max(maxans, ans);
            minans = min(minans, ans);
        }
        
        return;
    } 
    for(int i = 1; cnt - i >= m - k; i++) //給第k組分數,剩下的個數不能小於剩下的組數
    {
        b[k] = i;
        dfs(k + 1, cnt - i);
    }
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        a[i + n] = a[i]; //將數字陣列加長2倍
    }
    for(int i = 1; i <= 2 * n; i++)
    {
        s[i] = s[i - 1] + a[i]; //計算字首和
    }

    dfs(1, n);

    cout << minans << endl;
    cout << maxans << endl;

    return 0;
}

進一步思考,該題直觀上就是一個區間/環形DP問題,普通的區間問題是最終合併成一段,而此題最終分成m段

因此,狀態表示上,需要增加一維,變成三維。主要過程如下:

1、狀態表示

a[2 * N]表示原陣列,將環拆開成鏈,增長2倍

s[2 * N]表示字首和陣列,便於快速求一組資料的和

f[i][j][k]表示i ~ j分成k組,所得到的最大值

g[i][j][k]表示i ~ j分成k組,所得到的最小值

2、狀態轉移

考慮最後一組的位置,設最後一組的起始位置為l,則有

for(int i = 1; i <= n; i++)

  for(int j = i + 1; j < 2 * n; j++)

    for(int k = 2; k <= m; k++)

      for(int l = i+k-1; l <= j; l++) //前面k-1組至少k-1個數,最後一組最大從i+k-1開始

        f[i][j][k] = max(f[i][j][k], f[i][l-1][k-1] * f[l][j][1]) 

        g[i][j][k] = min(g[i][j][k], g[i][l-1][k-1] * g[l][j][1])        

3、初始化

f初始化為0,g初始化為極大值

memset(g, 0x3f, sizeof(g));

所有的f[i][j][1] = g[i][j][1] = ((s[j] - s[i-1]) % 10 + 10) % 10

4、結果

最大值:所有>=0的f[i][i+n-1][m]的最大值

最小值:所有>=0的g[i][i+n-1][m]的最小值

100分程式碼:

#include <bits/stdc++.h>
using namespace std;

const int N = 55, M = 10;
int n, m;
int a[2 * N]; //原數字,擴充2倍長度
int s[2 * N]; //字首和
int f[2 * N][2 * N][M];
int g[2 * N][2 * N][M];
int maxans = 0;
int minans = INT_MAX;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        a[i + n] = a[i]; //將數字陣列加長2倍
    }
    for(int i = 1; i <= 2 * n; i++)
    {
        s[i] = s[i - 1] + a[i]; //計算字首和
    }

    memset(g, 0x3f, sizeof(g));

    for(int i = 1; i <= n; i++)
    {
        for(int j = i; j < 2 * n; j++)
        {
             f[i][j][1] = g[i][j][1] = ((s[j] - s[i-1]) % 10 + 10) % 10;
        }
    }

    for(int i = 1; i <= n; i++)
    {
        for(int j = i + 1; j < 2 * n; j++)
        {
            for(int k = 2; k <= m; k++)
            {
               for(int l = i + k - 1; l <= j; l++) //最後一組的起始位置,預留k-1個數
                {
                    f[i][j][k] = max(f[i][j][k], f[i][l-1][k-1] * f[l][j][1]);
                    g[i][j][k] = min(g[i][j][k], g[i][l-1][k-1] * g[l][j][1]);
                }
            }
        }
    }

    for(int i = 1; i <= n; i++)
    {
        if(f[i][i+n-1][m] >= 0) maxans = max(maxans, f[i][i+n-1][m]);
        if(g[i][i+n-1][m] >= 0) minans = min(minans, g[i][i+n-1][m]);
    }
    cout << minans << endl;
    cout << maxans << endl;

    return 0;
}

相關文章