DP 练习集合
A. POJ 1923 Fourier's Lines
题意简述
试判断 n 条任意(不重合)直线是否可能形成 m 个交点,满足任意一个交点只有两条直线经过?如果是,并输出此种情况下,平面最多能被切成几份。
解题报告
直线之间产生交点当且仅当直线斜率不同
所以对于 \(n\) 条已经确定的直线,先把其中一类斜率全相同的直线拿出来,记有 \(i\) 条,那么剩下 \((n - i)\) 条直线的斜率一定和那 \(i\) 条都不相同,每一条都能贡献 \(i\) 个交点(一个交点只有两条直线经过),它们产生的贡献就是 \(i(n - i)\);而剩下的 \((n - i)\) 条直线内部又可以产生贡献,递归计算即可
实现是记忆化搜索
代码实现
int n, m;
char dp[100 + 10][10000 + 10]; // i 条线 j 个交点 是否存在方案
char dfs(int n, int m) {
// n 条直线里拿出一类斜率相同的直线共 i 条,剩下 (n - i) 条直线
// 交点个数为 i * (n - i) 加上 (n - i) 条直线内部形成的交点个数
if (m < 0 || m > ((n * (n - 1)) >> 1)) return 0;
if (!m) return 1;
if (dp[n][m] != -1) return dp[n][m];
for (int i = 1; i <= n - 1; ++i) {
if (dfs(n - i, m - i * (n - i)))
return dp[n][m] = 1;
} return dp[n][m] = 0;
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int cas = 0;
while (true) {
cin >> n >> m;
if (n == 0 && m == 0) break;
memset(dp, -1, sizeof dp);
cout << "Case " << ++cas << ": ";
if (dfs(n, m)) cout << n << " lines with exactly " << m << " crossings can cut the plane into " << n + m + 1 << " pieces at most." << endl;
else cout << n << " lines cannot make exactly " << m << " crossings." << endl;
}
return 0;
}
C. Increasing Sequences
题意简述
给定一个数字串,现在要给这个串添加逗号(也可以不加),使之成为一个严格递增的数字序列,并且最小化最后一个数字的长度。如果可以,最大化第一个数字的长度。输出方案。
解题报告
首先,最小化最后一个数字的长度意味着你要使每一段的长度都尽可能小,这和最大化第一个数字的长度是冲突的。
所以我们先不管第二个要求,设 \(f[i]\) 表示以第 \(i\) 个数字为结尾的最后一段,能获得的最大长度,也就是说 \(f[n]\) 就是最优答案。
注意到 DP 的一个特点:无后效性,无论我们之前是走的哪条路得到的这个答案,都不会影响后续的求解。因此,我们基于这个状态进行第二阶段的 DP。
设 \(g[i]\) 表示以 \(i\) 为开头的那一段的长度,注意此时的最优子结构是最大长度。显然,初始状态是 \(g[n - f[n] + 1] = f[n]\),我们要对 \(g\) 进行递推求解。
递推方程 \(f\) 和 \(g\) 都是一样的,枚举接下来一段的长度(或者端点),判断,更新答案
输出方案就根据 \(g\) 数组挨个输出即可
代码实现
const int MAXLEN = 80 + 10;
int n, aa[MAXLEN];
int dp[MAXLEN];
struct Seq {
int l, r; Seq(int _l = 0, int _r = 0) : l(_l), r(_r) {}
};
bool operator > (const Seq &a, const Seq &b) {
int al = a.l, ar = a.r, bl = b.l, br = b.r;
while (aa[al] == 0 && al <= ar) ++al;
while (aa[bl] == 0 && bl <= br) ++bl;
int la = ar - al + 1, lb = br - bl + 1;
if (la < lb) return false;
else if (la > lb) return true;
else {
for (int i = 1; i <= la; ++i) {
int xa = al + i - 1, xb = bl + i - 1;
if (aa[xa] > aa[xb]) return true;
else if (aa[xa] < aa[xb]) return false;
}
} return false;
}
void cleanup() {
memset(aa, 0, sizeof aa); memset(dp, 0, sizeof dp);
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
std::string input;
while (cin >> input) {
if (input.size() == 1 && input[0] == '0') break;
n = (int) input.size();
rep (i, 1, n) aa[i] = input[i - 1] - '0';
for (int i = 1; i <= n; ++i) {
dp[i] = i; // 此时,dp[i] 表示以第 i 个位置结尾的上一段长度
for (int j = i - 1; j >= 1; --j) {
if (Seq(j + 1, i) > Seq(j - dp[j] + 1, j)) {
dp[i] = i - j; break;
}
}
}
int lastlen = dp[n]; memset(dp, 0, sizeof dp);
dp[n - lastlen + 1] = lastlen; // 以刚刚求出的最优解为基础,倒着 dp
// 此时,dp[i] 表示以第 i 个位置为开头的这一段长度
for (int i = n - lastlen; i >= 1; --i) {
if (aa[i] == 0) dp[i] = dp[i + 1] + 1;
else for (int j = n - lastlen + 1; j > i; --j) {
if (Seq(j, j + dp[j] - 1) > Seq(i, j - 1)) {
dp[i] = j - i; break;
}
}
}
for (int i = 1, nowlen = dp[1] + 1; i <= n; ++i) {
if (i == nowlen) {
cout << ','; nowlen = i + dp[i];
}
cout << aa[i];
} cout << '\n';
cleanup();
}
return 0;
}
E. Strange Towers of Hanoi
题意简述
一个四塔的汉诺塔。
解题报告
子问题:枚举拿出多少盘子,将它们以四塔的操作方式放到另一个塔上
剩下的盘子以三塔的操作方式放到目标塔上,最后再把另一个塔上的盘子以四塔的操作方式放上来就行了
可以用记搜写,也可以递推
代码实现
int dp[13];
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
dp[1] = 1; cout << 1 << endl;
for (int i = 2; i <= 12; ++i) {
dp[i] = 0x7f7f7f7f;
for (int j = 1; j < i; ++j) {
// 枚举 j 个盘子放到另一个塔,最后放回来,这是一个已知答案的四塔问题
// 剩下的 i - j 个盘子是三塔问题
dp[i] = std::min(dp[i], 2 * dp[j] + ((1 << (i - j)) - 1));
}
cout << dp[i] << endl;
}
return 0;
}
F. Stock Exchange
最长上升子序列
const int MAXN = 100000 + 10;
int n;
int d[MAXN], len;
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
while (cin >> n) {
len = 0;
for (int i = 1; i <= n; ++i) {
int x; cin >> x;
if (i == 1) { d[++len] = x; continue; }
if (x > d[len]) d[++len] = x;
else {
int j = (std::lower_bound(d + 1, d + len + 1, x) - d);
d[j] = x;
}
} cout << len << endl;
}
return 0;
}
G. Fixed Points
题意简述
一个长度为 \(n\) 的序列 \(a\),现在可以删除一些数字(也可以不删),删完之后右面的数字编号会自动 \(- 1\),求问最少删除几个数字可以让这个序列中 \(a_i = i\) 的数字个数超过 \(k\),或判断无解,
解题报告
设 dp[i][j]
表示考虑前 \(i\) 个数字,删除 \(j\) 个数字后,\(a_i = i\) 最多有多少个。
对当前数字删或不删分别转移即可
代码实现
const int MAXN = 2000 + 10;
int n, k;
int aa[MAXN];
int dp[MAXN][MAXN]; // 前 i 个数字,删去 j 个
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int T; cin >> T;
while (T --> 0) {
cin >> n >> k;
rep (i, 1, n) cin >> aa[i];
memset(dp, 0, sizeof dp);
rep (i, 1, n) {
rep (j, 0, i) {
// 删与不删
dp[i][j] = std::max(dp[i - 1][std::max(0, j - 1)], dp[i - 1][j] + (aa[i] == i - j));
}
}
int ans = -1; rep (i, 0, n) {
if (dp[n][i] >= k) { ans = i; break; }
} cout << ans << endl;
}
return 0;
}
H. Equidistant Vertices
题意简述
一棵 \(n\) 个点的树,从中选择恰好 \(k\) 个点使得两两距离相同,求方案数。
解题报告
\(k = 2\) 时答案就是 \(\frac{n(n - 1)}{2}\)
\(k \geq 3\) 时,有一个结论:
必定存在一个点 \(C\),满足这个点集里的所有点到 \(C\) 的距离相等。
于是就考虑枚举这个点 \(C\),提起来当根,可选的点也就是同一深度的点;但是如果两个点 LCA 不是根的话就出问题了。
所以设 \(dp[i][j]\) 表示根的前 \(i\) 棵子树中,有 \(j\) 棵子树是贡献了一个点的,这种情况的方案数
转移就对当前子树选或不选分别转移即可
代码实现
const int MAXN = 100 + 10;
const int HA = 1e9 + 7;
int n, k; int ans;
std::vector<int> G[MAXN];
int siz[MAXN][MAXN]; // siz[u][dep(from 0)] : u 子树的第 dep 层有几个节点
int dp[MAXN][MAXN]; // 以 root 为根时,前 i 个子树中有 j 个子树贡献了点
void cleanup() {
ans = 0; rep (i, 1, n) G[i].clear();
}
void dfs(int u, int fa) {
siz[u][0] = 1;
forall (G[u], i) {
int v = G[u][i];
if (v == fa) continue;
dfs(v, u);
rep (j, 1, n) siz[u][j] += siz[v][j - 1];
// 直接循环到 n 其实没啥问题
}
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int T; cin >> T; while (T --> 0) {
cin >> n >> k;
rep (i, 1, n - 1) {
int u, v; cin >> u >> v;
G[u].push_back(v); G[v].push_back(u);
}
if (k == 2) { cout << (n * (n - 1) >> 1) << endl; cleanup(); continue;}
rep (root, 1, n) {
dfs(root, 0);
rep (dep, 1, n) {
std::vector<int> sizes;
forall (G[root], i) {
int u = G[root][i];
sizes.push_back(siz[u][dep - 1]);
}
dp[0][0] = 1;
rep (x, 1, (int) sizes.size()) {
dp[x][0] = 1;
rep (d, 1, x) {
// 转移:选与不选
dp[x][d] = (dp[x - 1][d] + 1ll * dp[x - 1][d - 1] * sizes[x - 1] % HA) % HA;
}
} ans = (ans + dp[sizes.size()][k]) % HA;
memset(dp, 0, sizeof dp);
sizes.clear();
} memset(siz, 0, sizeof siz);
} cout << ans << endl;
cleanup();
}
return 0;
}
I. Unstable String
题意简述
定义一个 01 字符串是美丽的,当且仅当它所有相邻两位是不同的
现在给定一个只含 0
、1
、?
的字符串,?
可以替换成 0
或 1
,求这个字符串最多有多少个子串是美丽的。
解题报告
考虑 dp。
设 f[i][0/1]
表示以第 \(i\) 位为结尾的极长美丽子串的长度,且第 \(i\) 位是 \(0/1\)。
转移对于 \(0\) 或 \(1\) 分别转移
统计答案只需要把每一个 \max(f[i][0], f[i][1])
加起来就好了
代码实现
const int MAXN = 2e5 + 10;
int n;
std::string s;
long long int dp[MAXN][2]; // dp[i][0/1] 以 i 为结尾,第 i 位是 0 / 1 的极长子串长度
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int T; cin >> T; while (T --> 0) {
cin >> s; n = (int) s.size(); s = " " + s;
rep (i, 1, n) {
if (s[i] == '0' || s[i] == '?') dp[i][0] = dp[i - 1][1] + 1;
if (s[i] == '1' || s[i] == '?') dp[i][1] = dp[i - 1][0] + 1;
}
long long int ans = 0;
rep (i, 1, n) ans += std::max(dp[i][0], dp[i][1]);
cout << ans << endl;
memset(dp, 0, sizeof dp);
}
return 0;
}
L. Parsa's Humongous Tree
题意简述
一棵树,每个点的点权范围是 \([l, r]\),一条边的边权为两个点点权之差绝对值,求整棵树的最大边权和是多少
解题报告
首先证明一个重要结论:每个点的点权只能是 \(l\) 或 \(r\)。
假设一个点周围的点权都已经确定,将这些权值放到一个数轴上,题目就转化成了找一个点到这些点距离之和最大。显然是距离中位数越远越好,也就是一定会取两个极端值。
之后就是普通 dp 了。设 \(f[i][0/1]\) 表示 \(i\) 点权值为 \(l(0),r(1)\) 时 \(i\) 为根的子树的最大答案。分别转移即可。
代码实现
const int MAXN = 2e5 + 10;
int n;
long long int dp[MAXN][2]; // 以 i 为根的子树,a[i] = l(0), r(1)
std::vector<int> G[MAXN];
std::pair<int, int> rgs[MAXN];
void dfs(int u, int fa) {
forall (G[u], i) {
int v = G[u][i];
if (v == fa) continue;
dfs(v, u);
dp[u][0] += std::max(dp[v][0] + abs(rgs[u].first - rgs[v].first), dp[v][1] + abs(rgs[u].first - rgs[v].second));
dp[u][1] += std::max(dp[v][0] + abs(rgs[u].second - rgs[v].first), dp[v][1] + abs(rgs[u].second - rgs[v].second));
}
}
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int T; cin >> T;
while (T --> 0) {
cin >> n;
rep (i, 1, n) cin >> rgs[i].first >> rgs[i].second;
rep (i, 1, n - 1) {
int u, v; cin >> u >> v;
G[u].push_back(v); G[v].push_back(u);
} dfs(1, 0);
cout << std::max(dp[1][0], dp[1][1]) << endl;
memset(dp, 0, sizeof dp);
rep (i, 1, n) G[i].clear();
}
return 0;
}
N. Armchairs
题意简述
人和椅子一共 n 个,编号从 1 到 n,其中有不超过 n/2 个人。现在要求每个人选一把椅子坐下,代价是编号之差的绝对值,求最小代价和。
解题报告
首先把人和椅子拿出来分别排序。
设 dp[i][j]
表示前 i 个人坐前 j 个椅子的最小代价。
转移就对第 i 个人坐、不坐第 j 个椅子分别转移即可
统计答案的时候把 dp[人数][1~椅子数]
都统计一下即可
代码实现
const int MAXN = 5000 + 10;
int n;
std::vector<int> mx[2];
std::vector<int> &pp = mx[1], &chr = mx[0];
int dp[MAXN][MAXN]; // 前 i 个人坐前 j 把椅子
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
rep (i, 1, n) {
int f; cin >> f; mx[f].push_back(i);
} memset(dp, 0x3f, sizeof dp);
rep (i, 0, (int) chr.size()) dp[0][i] = 0;
rep (i, 1, (int) pp.size()) {
rep (j, 1, (int) chr.size()) {
dp[i][j] = std::min(dp[i][j - 1], dp[i - 1][j - 1] + abs(mx[1][i - 1] - mx[0][j - 1]));
}
} int ans = 0x3f3f3f3f;
rep (i, 1, (int) chr.size()) {
ans = std::min(ans, dp[pp.size()][i]);
} cout << ans << endl;
return 0;
}
O. Phoenix and Computers
题意简述
现在有一排 n 台电脑,开启第 i 台和第 i + 2 台会自动开启第 i + 1 台。求开启全部电脑的方案数。
解题报告
dp。
观察到开启电脑的序列一定呈这样分布:
=为自动开启,|为手动开启
|||||=||||=||||=||||......
也就是,手动开启的是成段分布的,这就是一个子问题
设 f[i][j] 表示开启了前 i 台电脑,其中手动开启了 j 台电脑,且这 i 台电脑的一个后缀都手动打开(显然最后一个必须手动打开,因为右边没有电脑),的方案数
考虑打开第 i + 2 台电脑往后的一串电脑,使第 i + 1 台电脑自动打开。这里计算方案数需要两个值:手动打开往后一串电脑的方案数,把这一串电脑的打开序列合并进以前打开序列的方案数。
结论:手动打开一串长为 \(n\) 的电脑的方案数是 \(2^{n - 1}\)。
枚举最先打开第 \(i\) 台电脑,接下来就是往序列里剩下的 \(n - 1\) 个空依次填放右边的 \(n - i\) 台电脑,剩下的空就用来填左边的,方案数即为 \(C_{n - 1}^{n - 1}\)
也就是 \(\sum_{i = 1}^n C_{n - 1}^{n - i} = 2^{n - 1}.\)
结论:按顺序将一个长为 \(n\) 的序列合进一个长为 \(m\) 的序列的方案数是 \(C_{n + m}^n.\)
可以直接隔板法。\(n + m\) 个空里选 \(n\) 个。
代码实现
const int MAXN = 400 + 10;
int HA = 0;
int n;
int dp[MAXN][MAXN]; // 开启了前 i 台电脑,其中有 j 台电脑是手动开的,且第 i 台一定是手动开的
int fac[MAXN], invfac[MAXN], twoi[MAXN];
int fastPow(int a, int b, int p) {
int ret = 1; while (b) {
if (b & 1) ret = 1ll * ret * a % p;
a = 1ll * a * a % p;
b >>= 1;
} return ret;
}
int C(int n, int m) { return 1ll * fac[n] * invfac[m] % HA * invfac[n - m] % HA; }
int main() {
n = read(); HA = read();
twoi[0] = fac[0] = 1; rep (i, 1, MAXN - 1) fac[i] = 1ll * fac[i - 1] * i % HA, twoi[i] = 1ll * twoi[i - 1] * 2 % HA;
invfac[MAXN - 1] = fastPow(fac[MAXN - 1], HA - 2, HA);
for (int i = MAXN - 2; i >= 1; --i) invfac[i] = 1ll * invfac[i + 1] * (i + 1) % HA;
for (int i = 1; i <= n; ++i) {
dp[i][i] = twoi[i - 1];
for (int j = 0; j <= i; ++j) {
for (int k = 1; i + k + 1 <= n; ++k) {
// 手动开启 i + 2 ~ i + k + 1,共 k 台电脑,使其自动开启第 i + 1 台电脑
// twoi[k - 1] 是开启 k 台电脑的方案数
// C(j + k, j) 是把这 k 台电脑开启序列合并进前 j 台电脑开启序列的方案数
dp[i + k + 1][j + k] = (1ll * dp[i + k + 1][j + k] + 1ll * dp[i][j] * twoi[k - 1] % HA * C(j + k, k) % HA) % HA;
}
}
} int ans = 0;
for (int i = 1; i <= n; ++i) {
ans = (1ll * ans + dp[n][i]) % HA;
} printf("%d\n", ans);
return 0;
}
P. Maximum Sum of Products
题意简述
两个序列 \(a,b\),现在可以翻转至多一个子串,求最大的 \(\sum_{i = 1}^n a_i \times b_i.\)
解题报告
这题没有最优子结构,但可以用类似 dp 的思想解决,一个区间的递推
设 \(f[i][j]\) 表示翻转 \([i, j]\) 的答案,根据区间 dp 的一般套路,考虑枚举区间中点和长度(对奇数和偶数分别转移),很容易写出转移方程:
预处理:显然,初始状态是 \(f[i][i]\) 和 \(f[i][i + 1]\),所有状态都是这些状态转移出来的,预处理显然不是难事
最后统计一遍 \(\max\{f_{i,j}\}\)
代码实现
const int MAXN = 5000 + 10;
int n;
lli aa[MAXN], bb[MAXN];
lli dp[MAXN][MAXN];
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
rep (i, 1, n) cin >> aa[i];
rep (i, 1, n) cin >> bb[i];
for (int i = 1; i <= n; ++i) {
dp[1][1] += aa[i] * bb[i];
} for (int i = 2; i <= n; ++i) dp[i][i] = dp[i - 1][i - 1];
for (int i = 1; i <= n - 1; ++i) {
dp[i][i + 1] = dp[i][i]
- aa[i] * bb[i]
- aa[i + 1] * bb[i + 1]
+ aa[i] * bb[i + 1]
+ aa[i + 1] * bb[i];
}
for (int mid = 1; mid <= n; ++mid) {
for (int len = 2; len <= n; ++len) {
int l = mid - len + 1, r = mid + len - 1;
if (l < 1 || r > n) break;
dp[l][r] = dp[l + 1][r - 1]
- aa[l] * bb[l] - aa[r] * bb[r]
+ aa[l] * bb[r] + aa[r] * bb[l];
}
}
for (int mid = 1; mid <= n; ++mid) {
for (int len = 2; len <= n; ++len) {
int l = mid - len, r = mid + len - 1;
if (l < 1 || r > n) break;
dp[l][r] = dp[l + 1][r - 1]
- aa[l] * bb[l] - aa[r] * bb[r]
+ aa[l] * bb[r] + aa[r] * bb[l];
}
} lli ans = 0;
for (int i = 1; i <= n; ++i) {
for (int j = i; j <= n; ++j) {
ans = std::max(ans, dp[i][j]);
// printf("%lld%c", dp[i][j], j == n ? '\n' : ' ');
}
} cout << ans << endl;
return 0;
}
1. HDU 6968 I love exam
题意简述
共 \(n\) 科的考试前有 \(t\) 天时间用于复习,给定 \(m\) 本书,每本书可以让它对应的科目成绩提高一定值,同时会花费一定天数(同一天只能读同一本书,每本书只能读一次)。
一个科目挂科的充要条件是它的成绩 \(< 60\)。假设你在不复习的情况下每科都只有 0 分,请问有没有一种复习的方案,使得挂科的科目数小于 \(p\),且总成绩最高?如果有,输出总成绩;如果没有,输出 -1.
解题报告
又是一个两次 DP 的题目。
第一次 DP:设 \(f[i][j]\) 表示第 \(i\) 科要拿到 \(j\) 分最少花费多少时间。一个很简单的背包。
第二次 DP:设 \(g[i][j][k]\) 表示前 \(i\) 科,花费 \(j\) 天来复习,最终有 \(k\) 科挂科,能获得的最大成绩。转移其实和背包也差不多,对挂不挂科分别处理,用 \(f\) 数组作为背包的数据。
代码实现
const int MAX_UP = 200;
const int MAXN = 50 + 10;
const int MAXT = 500 + 10;
const int MAXM = 15000 + 10;
const int MAXSCR = 200 + 10;
int n, m, t, p;
std::map<std::string, int> idx;
int f[MAXN][MAXSCR]; // f[i][j] 表示第 i 课考到分数为 j 最少需要多少时间,是个背包
int g[MAXN][MAXT][5]; // g[i][j][k] 表示前 i 门课,前 j 天,挂了 k 课,获得的最高成绩
struct E {
int id, impscr, day;
E(int _i = 0, int _p = 0, int _d = 0) : id(_i), impscr(_p), day(_d) {}
} es[MAXM];
int main() {
std::ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int T; cin >> T;
while (T --> 0) {
cin >> n;
rep (i, 1, n) {
std::string s; cin >> s; idx[s] = i;
for (int j = 0; j <= MAX_UP; ++j) f[i][j] = 0x3f3f3f3f;
f[i][0] = 0;
}
cin >> m;
rep (i, 1, m) {
std::string s; cin >> s; es[i].id = idx[s];
cin >> es[i].impscr >> es[i].day;
}
cin >> t >> p;
for (int i = 1; i <= m; ++i) {
int id = es[i].id;
for (int j = MAX_UP; j >= es[i].impscr; --j) {
f[id][j] = std::min(f[id][j], f[id][j - es[i].impscr] + es[i].day);
}
}
for (int i = 1; i <= n; ++i) {
for (int k = 0; k <= std::min(i, p); ++k) {
if (k == 0) {
for (int sc = MAX_UP; sc >= 60; --sc) {
for (int j = t; j >= f[i][sc]; --j) {
g[i][j][k] = std::max(g[i][j][k], g[i - 1][j - f[i][sc]][k] + std::min(sc, 100));
}
}
} else {
for (int sc = MAX_UP; sc >= 0; --sc) {
for (int j = t; j >= f[i][sc]; --j) {
g[i][j][k] = std::max(g[i][j][k], g[i - 1][j - f[i][sc]][k - (sc < 60)] + std::min(sc, 100));
}
}
}
}
}
int ans = 0;
for (int i = 1; i <= t; ++i) {
for (int j = 0; j <= p; ++j) {
ans = std::max(ans, g[n][i][j]);
}
}
if (ans >= (n - p) * 60) cout << ans << endl;
else cout << -1 << endl;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= t; ++j) {
for (int k = 0; k <= p; ++k) {
g[i][j][k] = 0;
}
}
}
idx.clear();
}
return 0;
}