比赛链接:21.7.16-NUAA暑期集训
比赛码:NUAAACM20210716
A - Mr. Young's Picture Permutations
注意到(k)很小,我们就可以暴力状态了,而要表现单调递增,故维护一个从左至右边矮的阶梯。
以有(5)列为例,设(f[a][b][c][d][e])表示第(1)列已经填的数字高度为(a),第(2)列高度为(b),...,第(5)列的高度为(e)的,现在填到数字(a+b+c+d+e)方案数,并保证转移时(a geq b geq c geq d geq e),于是当一个新的数字填的时候一定比所有的数字都要大。
递推式为:
边界:(f[0][0][0][0][0]=1),其余为(0)。
答案:(f[n1][n2][n3][n4][n5])。
#include <cstdio>
#include <cstring>
using namespace std;
int row, n[6];
int main() {
while (scanf("%d", &row) && row) {
memset(n, 0, sizeof(n));
for (int i = 1; i <= row; ++i) {
scanf("%d", &n[i]);
}
unsigned int f[n[1] + 1][n[2] + 1][n[3] + 1][n[4] + 1][n[5] + 1];
memset(f, 0, sizeof(f));
f[0][0][0][0][0] = 1;
for (int i1 = 0; i1 <= n[1]; ++i1) {
for (int i2 = 0; i2 <= n[2]; ++i2) {
for (int i3 = 0; i3 <= n[3]; ++i3) {
for (int i4 = 0; i4 <= n[4]; ++i4) {
for (int i5 = 0; i5 <= n[5]; ++i5) {
unsigned int t = f[i1][i2][i3][i4][i5];
if (i1 < n[1]) {
f[i1 + 1][i2][i3][i4][i5] += t;
}
if (i2 < n[2] && i1 > i2) {
f[i1][i2 + 1][i3][i4][i5] += t;
}
if (i3 < n[3] && i2 > i3) {
f[i1][i2][i3 + 1][i4][i5] += t;
}
if (i4 < n[4] && i3 > i4) {
f[i1][i2][i3][i4 + 1][i5] += t;
}
if (i5 < n[5] && i4 > i5) {
f[i1][i2][i3][i4][i5 + 1] += t;
}
}
}
}
}
}
printf("%u
", f[n[1]][n[2]][n[3]][n[4]][n[5]]);
}
return 0;
}
B - 石子合并
区间dp模板题,由于是圆形操场,所以将数据再拓展一遍,用前缀和存储石子堆数量。
#include <cstdio>
#include <iostream>
using namespace std;
#define INF 0x3f3f3f3f
int n, ansmin, ansmax, dpmax[300][300], dpmin[300][300], num[300], sum[300];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", &num[i]);
num[i + n] = num[i];
}
for (int i = 1; i <= 2 * n; ++i) {
sum[i] = sum[i - 1] + num[i];
}
for (int len = 1; len < n; ++len) {
for (int i = 1; i + len <= 2 * n; ++i) {
int j = i + len;
dpmin[i][j] = INF, dpmax[i][j] = 0;
for (int k = i; k < j; ++k) {
dpmax[i][j] = max(dpmax[i][j], dpmax[i][k] + dpmax[k + 1][j] + sum[j] - sum[i - 1]);
dpmin[i][j] = min(dpmin[i][j], dpmin[i][k] + dpmin[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
ansmin = INF, ansmax = 0;
for (int i = 1; i <= n; i++) {
ansmax = max(ansmax, dpmax[i][i + n - 1]);
ansmin = min(ansmin, dpmin[i][i + n - 1]);
}
cout << ansmin << endl << ansmax << endl;
return 0;
}
C - 小奇采药
乍一看以为是背包模板题,但看到数据(m)的范围就知道背包会超时,于是考虑搜索并剪枝。
先按时间从大到小排序,求一个后缀和,(lastw[i])表示(i)到(n)的时间和,(lastv[i])表示(i)到(n)的价值和,然后跑(dfs)。
以下三种情况可以剪枝:
- 如果当前的时间和加上(a[n].w)还超过了背包容量,也就是不管用哪一件往背包里塞都装不下了,就退出;
- 如果当前的价值和加上(lastv[k])也就是剩下的价值和仍然小于最优值,就退出;
- 如果当前的时间和加上(lastw[k])也就是剩下的时间和仍然小于背包容量,就拿当前的价值和加上(lastv[k])和原最优解比较,如果当前的更优,更新答案,这时候就没有再递归下去的必要了,直接退出。
#include <algorithm>
#include <cstdio>
#include <iostream>
using namespace std;
#define ll long long
ll ans = 0, m, lastw[155], lastv[155];
int n, t;
struct node {
int w, v;
bool operator<(const node &obj) const { return w > obj.w; }
} a[155];
void dfs(int k, ll sumw, ll sumv) {
ans = max(ans, sumv);
if (sumw + a[n].w > m || sumv + lastv[k] <= ans) {
return;
}
if (sumw + lastw[k] <= m) {
ans = max(ans, sumv + lastv[k]);
return;
}
if (sumw + a[k].w <= m) {
dfs(k + 1, sumw + a[k].w, sumv + a[k].v);
}
dfs(k + 1, sumw, sumv);
}
int main() {
scanf("%d", &t);
while (t--) {
scanf("%d%lld", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%d%d", &a[i].w, &a[i].v);
}
sort(a + 1, a + n + 1);
lastw[n + 1] = 0;
lastv[n + 1] = 0;
for (int i = n; i >= 1; i--) {
lastw[i] = lastw[i + 1] + a[i].w;
lastv[i] = lastv[i + 1] + a[i].v;
}
ans = 0;
dfs(1, 0, 0);
printf("%lld
", ans);
}
return 0;
}
D - 拦截导弹
最多能拦截多少导弹就是求最长不上升子序列,最少要配备多少套系统就是求最长上升序列。
#include <algorithm>
#include <iostream>
using namespace std;
#define N 100010
int a[N], f1[N], f2[N], n;
int main() {
while (cin >> a[++n]);
n--;
int len1 = 1, len2 = 1;
f1[1] = a[1];
f2[1] = a[1];
for (int i = 2; i <= n; ++i) {
if (f1[len1] >= a[i]) {
f1[++len1] = a[i];
} else {
int p1 = upper_bound(f1 + 1, f1 + 1 + len1, a[i], greater<int>()) - f1;
f1[p1] = a[i];
}
if (f2[len2] < a[i]) {
f2[++len2] = a[i];
} else {
int p2 = lower_bound(f2 + 1, f2 + 1 + len2, a[i]) - f2;
f2[p2] = a[i];
}
}
cout << len1 << endl << len2;
return 0;
}
E - 合唱队形
正反各求一次最长上升序列,对每个点取正反两次以该点为最高点的最长上升子序列长度之和(注意该点被取两次,需要减一)即为以该点为最高点的最长合唱队列。
#include <algorithm>
#include <iostream>
using namespace std;
#define N 110
int n, dp1[N], dp2[N], h[N], t1[N], t2[N], len1, len2, ans;
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> h[i];
}
dp1[++len1] = h[1];
t1[1] = 1;
for (int i = 1; i <= n; ++i) {
if (dp1[len1] < h[i]) {
dp1[++len1] = h[i];
t1[i] = len1;
} else {
int p1 = lower_bound(dp1 + 1, dp1 + len1 + 1, h[i]) - dp1;
dp1[p1] = h[i];
t1[i] = p1;
}
}
dp2[++len2] = h[n];
t2[n] = 1;
for (int i = n; i > 0; --i) {
if (dp2[len2] < h[i]) {
dp2[++len2] = h[i];
t2[i] = len2;
} else {
int p2 = lower_bound(dp2 + 1, dp2 + len2 + 1, h[i]) - dp2;
dp2[p2] = h[i];
t2[i] = p2;
}
}
for (int i = 1; i <= n; ++i) {
ans = max(ans, t1[i] + t2[i] - 1);
}
cout << n - ans;
return 0;
}
F - Cash Machine
多重背包模板题,需要用到二进制拆分,将物品数(n_i)拆成(1,2,4,8,...,2^{k},x(x leq 2^{k + 1})),则这些数的组合可以表示([1,n_i])的所有数,就不需要循环从(1)跑到(n_i)了。
#include <cstring>
#include <iostream>
using namespace std;
#define N 110
int dp[100010], cash, n, cnt, ni, di;
struct Node {
int w, v;
} node[110];
void addNode(int n, int d) {
int p = 1;
while (n) {
if (n >= p) {
node[++cnt].w = d * p;
node[cnt].v = d * p;
n -= p;
p <<= 1;
} else {
node[++cnt].w = d * n;
node[cnt].v = d * n;
n = 0;
}
}
}
int main() {
while (cin >> cash >> n) {
memset(dp, 0, sizeof(dp));
cnt = 0;
for (int i = 1; i <= n; ++i) {
cin >> ni >> di;
addNode(ni, di);
}
for (int i = 1; i <= cnt; ++i) {
for (int j = cash; j >= node[i].w; --j) {
dp[j] = max(dp[j], dp[j - node[i].w] + node[i].v);
}
}
cout << dp[cash] << endl;
}
return 0;
}
G - Coins
设(dp[i][j])为前(i)种面值的硬币组成面额(j)时,第(i)种硬币剩余的数量。
默认(dp[i][j] = -1),表示无法组成面额(j)。
若(dp[i - 1][j]geq 0),则说明已经可以组成该面额,不需要第(i)种硬币,所以(dp[i][j] = c[i])。
否则就看(dp[i][j-a[i]])的值,若(j < a[i]),显然无法通过第(i)种硬币组成面额(j),则(dp[i][j]=-1);若(dp[i][j-a[i]]leq 0),则表示第(i)种硬币已用完,则(dp[i][j]=-1);否则(dp[i][j]=dp[i][j-a[i]]-1)。
可以将二维数组压缩为一维。
#include <cstring>
#include <iostream>
using namespace std;
int n, m, dp[100010], a[110], c[110], ans;
int main() {
while (cin >> n >> m && (n || m)) {
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
for (int i = 1; i <= n; ++i) {
cin >> c[i];
}
memset(dp, -1, sizeof(dp));
dp[0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
if (dp[j] >= 0) {
dp[j] = c[i];
} else if (j < a[i] || dp[j - a[i]] <= 0) {
dp[j] = -1;
} else {
dp[j] = dp[j - a[i]] - 1;
}
}
}
ans = 0;
for (int i = 1; i <= m; ++i) {
if (dp[i] >= 0) {
ans++;
}
}
cout << ans << endl;
}
return 0;
}
H - 能量项链
一道典型的区间(dp)题。
状态转移方程:
#include <iostream>
using namespace std;
#define N 1010
int a[N], n;
long long dp[N][N], ans;
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
a[n + i] = a[i];
}
for (int len = 1; len < n; ++len) {
for (int i = 1; i + len < 2 * n; ++i) {
int j = i + len;
dp[i][j] = 0;
for (int k = i; k < j; ++k) {
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + a[i] * a[k + 1] * a[j + 1]);
}
}
}
for (int i = 1; i <= n; ++i) {
ans = max(ans, dp[i][i + n - 1]);
}
cout << ans;
return 0;
}
I -
J -
K - Polygon
区间(dp)题。
1. 如果操作符是“(+)”:
区间([L, R])的最大值(=)左区间([L, K])的最大值(+)右区间([K + 1, R])的最大值;
区间([L, R])的最小值(=)左区间([L, K])的最小值(+)右区间([K + 1, R])的最小值。
2. 如果操作符是“( imes)”:
(minL)表示左区间([L, K])的最小值,
(maxL)表示左区间([L, K])的最大值,
(minR)表示右区间([K + 1, R])的最小值,
(maxR)表示右区间([K + 1, R])的最大值,
区间([L, R])的最大值(=max{minL * minR, minL * maxR, maxL * minR, maxL * maxR});
区间([L, R])的最小值(=min{minL * minR, minL * maxR, maxL * minR, maxL * maxR})。
注意(K)的取值范围(Lleq K < R)。
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
#define INF 0x3f3f3f3f
int dpmax[110][110], dpmin[110][110], n, ans;
char oper[110];
vector<int> vec;
int max_4(int a, int b, int c, int d) {
return max(a * c, max(a * d, max(b * c, b * d)));
}
int min_4(int a, int b, int c, int d) {
return min(a * c, min(a * d, min(b * c, b * d)));
}
int main() {
cin >> n;
memset(dpmax, -0x3f, sizeof(dpmax));
memset(dpmin, 0x3f, sizeof(dpmin));
for (int i = 1; i <= n; ++i) {
cin >> oper[i] >> dpmax[i][i];
oper[i + n] = oper[i];
dpmin[i][i] = dpmin[i + n][i + n] = dpmax[i + n][i + n] = dpmax[i][i];
}
for (int len = 2; len <= n; ++len) {
for (int i = 1; i + len - 1 < 2 * n; ++i) {
int j = i + len - 1;
for (int k = i; k < j; ++k) {
if (oper[k + 1] == 't') {
dpmax[i][j] = max(dpmax[i][j], dpmax[i][k] + dpmax[k + 1][j]);
dpmin[i][j] = min(dpmin[i][j], dpmin[i][k] + dpmin[k + 1][j]);
} else {
dpmax[i][j] = max(dpmax[i][j], max_4(dpmax[i][k], dpmin[i][k], dpmax[k + 1][j], dpmin[k + 1][j]));
dpmin[i][j] = min(dpmin[i][j], min_4(dpmax[i][k], dpmin[i][k], dpmax[k + 1][j], dpmin[k + 1][j]));
}
}
}
}
ans = -INF;
for (int i = 1; i <= n; ++i) {
if (ans < dpmax[i][i + n - 1]) {
ans = dpmax[i][i + n - 1];
vec.clear();
vec.push_back(i);
} else if (ans == dpmax[i][i + n - 1]) {
vec.push_back(i);
}
}
cout << ans << endl;
for (int i = 0; i < vec.size(); ++i) {
cout << vec[i] << ' ';
}
return 0;
}
L - 飞扬的小鸟
按照横坐标从左往右递推,设(dp[i][j])为到达点((i,j))所需的最小点击次数。
状态转移方程:
因为触顶之后不会再上升,所以(dp[i][m] = min{dp[i][k],mleq k leq m+x[i]})。
如果所在的点有管道,则(dp[i][j]=0)。
#include <cstring>
#include <iostream>
using namespace std;
#define INF 0x3f3f3f3f
#define N 10010
#define M 2010
int n, m, p, x[N], y[N], low[N], high[N], dp[N][M], ans;
bool vis[N];
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m >> p;
for (int i = 1; i <= n; ++i) {
cin >> x[i] >> y[i];
}
for (int i = 1; i <= n; ++i) {
low[i] = 1, high[i] = m;
}
for (int i = 1, k, l, h; i <= p; ++i) {
cin >> k >> l >> h;
vis[k] = true;
low[k] = l + 1;
high[k] = h - 1;
}
memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= m; ++i) {
dp[0][i] = 0;
}
for (int i = 1; i <= n; ++i) {
for (int j = x[i] + 1; j <= m + x[i]; ++j) {
dp[i][j] = min(dp[i][j], min(dp[i - 1][j - x[i]] + 1, dp[i][j - x[i]] + 1));
}
for (int j = m + 1; j <= m + x[i]; ++j) {
dp[i][m] = min(dp[i][m], dp[i][j]);
}
for (int j = 1; j <= m - y[i]; ++j) {
dp[i][j] = min(dp[i][j], dp[i - 1][j + y[i]]);
}
for (int j = 1; j < low[i]; ++j) {
dp[i][j] = dp[0][0];
}
for (int j = high[i] + 1; j <= m; ++j) {
dp[i][j] = dp[0][0];
}
}
ans = INF;
for (int i = 1; i <= m; ++i) {
ans = min(ans, dp[n][i]);
}
if (ans < INF) {
cout << 1 << endl << ans << endl;
} else {
int i, j;
for (i = n; i >= 1; --i) {
for (j = 1; j <= m; ++j) {
if (dp[i][j] < INF) {
break;
}
}
if (j <= m) {
break;
}
}
ans = 0;
for (j = 1; j <= i; ++j) {
if (vis[j]) {
ans++;
}
}
cout << 0 << endl << ans << endl;
}
return 0;
}
M - 采药
(01)背包模板题。
#include <iostream>
using namespace std;
int dp[1010], t, m, w, v;
int main() {
cin >> t >> m;
for (int i = 1; i <= m; ++i) {
cin >> w >> v;
for (int j = t; j >= w; --j) {
dp[j] = max(dp[j], dp[j - w] + v);
}
}
cout << dp[t];
return 0;
}