原題連結: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初始化為極大值
所有的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;
}