Problem
Northwestern European Regional Contest 2016
vjudge.net/problem/Gym-101170A
Meaning
给出 n 个位数为 m 的数字(前导零补足),可以通过改变某些数上的某些位置上的数字,使得该 n 个数字是非降序排列。求在总改动位数最少的情况下,改完后的 n 个数字(m 位)。
Analysis
一个很神奇的DP:
dp[i][j]:对前 i 个数做 j 次修改后得到的(满足非降序前提下)最小的第 i 个数
从合法的 dp[i][j] 开始推 dp[i+1][j+k]:尝试在 dp[i][j] 基础上再加 k 次操作,贪心地构造出尽可能小的数(先尝试跟 dp[i][j] 相等,不行的话就找个尽量低的位 +1,然后该位后面尽量置 0),来更新 dp[i+1][j+k],并记住转移的路径。
最后在 dp[n][0] ~ dp[n][?] 里找第一个合法的状态,并根据转移路径将找出答案序列。‘?’是指最多的总操作次数,极限是
N×M
,但官方题解说不超过
N×log10N
,而
1<log1040<2
。
Official Solution
- Let’s consider whole numbers without splitting them by digits.
- ai,j = the minimum number obtainable for i-th number, if we made j changes on the first i numbers.
- From any state we can try changing some amount of digits in the i +1-th number.
- Then we can greedily in O(M) obtain the smallest number we can get from i +1-th number using fixed number of changes.
- If it’s greater or equal than ai,j – that’s a valid transition.
- O(N2⋅M) for the state and O(M2) for the transition.
- But the answer is never going to be more than N⋅log10N .
- O(N2⋅log10N) for the state and O(N⋅log10N⋅M) for the transition.
Tips
- 按 dalao 的代码来看,从 dp[i][j] 推 dp[i+1][j+k] 时,好像不能保证 k 次修改机会一定会被用完,所以 dp 的定义应该是:对前 i 个数做不超过 j 次修改后得到的(满足非降序前提下)最小的第 i 个数
- 题目应该有 special judge?我的代码跑不出第二个样例的参考答案(而是更优),也过了
Code
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 40, M = 400;
char v[N+1][M+2]; // origin number string
char tmp[M+2]; // constructed number string
char dp[N+1][N<<1][M+2]; // or "string dp[N+1][N<<1]"
bool valid[N+1][N<<1]; // whether dp[i][j] is reachable
char *ans[N+1]; // answer sequence
int pre[N+1][N<<1]; // pre-status mark
bool construct(char *now, char *nxt, int m, int k)
{
// Plan A:
// get close to "now"
int num = k, pos;
strcpy(tmp, nxt);
if(!k) // special judge whether k = 0
return strcmp(tmp, now) >= 0;
for(pos = 0; pos < m && num > 0; ++pos)
if(tmp[pos] != now[pos])
{
tmp[pos] = now[pos];
--num;
}
if(strcmp(tmp, now) >= 0)
return true;
// Plan B:
// +1 at smallest possible digit (where "pos" is)
// and set most possible 0 after that
// can't +1 at digit '9'
for(--pos; pos >= 0 && now[pos] == '9'; )
--pos;
if(pos < 0)
return false;
num = k;
strcpy(tmp, nxt);
// keep consistency with "now" before "pos"
for(int i = 0; i < pos /* && num > 0 */; ++i)
if(tmp[i] != now[i])
{
tmp[i] = now[i];
--num;
}
// +1 at "pos"
if(tmp[pos] != now[pos] + 1)
{
tmp[pos] = now[pos] + 1;
--num;
}
// set 0 after "pos"
for(++pos; pos < m && num > 0; ++pos)
if(tmp[pos] != '0')
{
tmp[pos] = '0';
--num;
}
return true;
}
int main()
{
int n, m;
scanf("%d%d%*c", &n, &m);
for(int i = 1; i <= n; ++i)
scanf("%s", v[i]);
memset(valid, false, sizeof valid);
for(int i = 0; i < m; ++i)
dp[0][0][i] = '0';
dp[0][0][m] = '\0';
valid[0][0] = true;
for(int i = 0; i < n; ++i)
for(int j = 0; j < n << 1; ++j)
{
if(!valid[i][j])
continue;
for(int k = 0; k <= m && j + k < n << 1; ++k)
if(construct(dp[i][j], v[i+1], m, k) &&
(!valid[i+1][j+k] || strcmp(tmp, dp[i+1][j+k]) < 0))
{
valid[i+1][j+k] = true;
strcpy(dp[i+1][j+k], tmp);
pre[i+1][j+k] = j;
}
}
int p;
for(p = 0; p < n << 1; ++p)
if(valid[n][p])
break;
for(int i = n; i > 0; p = pre[i][p], --i)
ans[i] = dp[i][p];
for(int i = 1; i <= n; ++i)
puts(ans[i]);
return 0;
}