Heldivis
2024-11-04 16:13:58
by Heldivis & Redamancy_Lydic & zhujiangyuan
发现
将
设
对于
这样做复杂度是
发现可以从外向内转移,也就是倒着选,
转移:
答案:
sort (a + 1, a + n + 1);
memset (dp, 0x3f, sizeof (dp));
dp[1][n] = a[n] - a[1];
for (int i = 1; i <= n; i++)
for (int j = n; j >= i; j--)
if (i != 1 || j != n) dp[i][j] = min (dp[i][j + 1], dp[i - 1][j]) + a[j] - a[i];
for (int i = 1; i <= n; i++) ans = min (ans, dp[i][i]);
cout << ans;
<h4>区间 DP - 平方复杂度</h4>
考虑一个区间
先手必胜条件:
先手选
必胜条件是后手不管选
先手选
必胜条件是后手不管选
平局条件:
先手选
必胜条件是后手不管选
先手选
必胜条件是后手不管选
int Fn(int l, int r) {
if (l > r) return 0;
if (mp.count({l, r})) return mp[{l, r}];
int ll = Fn(l + 2, r), lr = Fn(l + 1, r - 1), rr = Fn(l, r - 2);
if ((ll == 1 || (ll == 0 && s[l] < s[l + 1])) && (lr == 1 || (lr == 0 && s[l] < s[r])))
return mp[{l, r}] = 1; // 先手选 l, 后手必败
if ((rr == 1 || (rr == 0 && s[r] < s[r - 1])) && (lr == 1 || (lr == 0 && s[r] < s[l])))
return mp[{l, r}] = 1; // 先手选 r, 后手必败
if ((ll == 1 || (ll == 0 && s[l] <= s[l + 1])) && (lr == 1 || (lr == 0 && s[l] <= s[r])))
return mp[{l, r}] = 0; // 先手选 l, 后手必不赢
if ((rr == 1 || (rr == 0 && s[r] <= s[r - 1])) && (lr == 1 || (lr == 0 && s[r] <= s[l])))
return mp[{l, r}] = 0; // 先手选 r, 后手必不赢
return mp[{l, r}] = -1;
}
<h4>推性质 - 线性复杂度</h4>
发现先手必不败。因为只剩最后两个时,只要先取走不大的一个,那么要么平局,要么先手赢。
然后发现,回文、两两重复或回文套两两重复时平局,其余先手胜。
scanf("%s", s + 1), l = 1, r = strlen(s + 1);
while (l < r && s[l] == s[r]) ++l, --r;
if (l >= r) return puts("Draw"), void();
for (int i = l; i + 1 <= r; i += 2) if (s[i] != s[i + 1]) return puts("Alice"), void();
puts("Draw");
其任意一个节点的左子树内所有节点编号都小于它,右子树内所有节点编号都大于它。所以一个区间是一个完整的子树。
考虑枚举一个区间的根
转移方程:
ll Calc(int sx, int fx, int sy, int fy) {
if (sx > fx || sy > fy) return 0;
return s[fx][fy] - s[fx][sy - 1] - s[sx - 1][fy] + s[sx - 1][sy - 1];
}
void Dfs(int l, int r, int p) {
if (l > r) return void();
if (l == r) return ans[l] = p, void();
ans[fa[l][r]] = p;
Dfs(l, fa[l][r] - 1, fa[l][r]), Dfs(fa[l][r] + 1, r, fa[l][r]);
}
for (int len = 2; len <= n; ++len) {
for (int l = 1, r; (r = l + len - 1) <= n; ++l) {
for (int k = l; k <= r; ++k) {
ll tmp = (k != l) * f[l][k - 1] + (k != r) * f[k + 1][r] +
Calc(l, k - 1, 1, l - 1) + Calc(l, k - 1, k, n) + Calc(k + 1, r, 1, k) + Calc(k + 1, r, r + 1, n);
if (f[l][r] > tmp) f[l][r] = tmp, fa[l][r] = k;
}
}
}
Dfs(1, n, 0);
因为交作业不需要时间,所以交一个区间的作业的时候,一定是先交左端点或者先交右端点。
设
初始:
转移:从相邻区间的两个状态转移而来。
答案为
时间复杂度
sort (a + 1, a + n + 1, [] (Node a, Node b) { return a.x < b.x; });
memset (dp, 0x3f, sizeof (dp));
dp[1][n][0] = max (a[1].x, a[1].t);
dp[1][n][1] = max (a[n].x, a[n].t);
for (int i = 1; i <= n; ++i)
for (int j = n; j >= i; j--) {
if (i == 1 && j == n) continue;
dp[i][j][0] = max (a[i].t, min (dp[i - 1][j][0] + a[i].x - a[i - 1].x, dp[i][j + 1][1] + a[j + 1].x - a[i].x));
dp[i][j][1] = max (a[j].t, min (dp[i - 1][j][0] + a[j].x - a[i - 1].x, dp[i][j + 1][1] + a[j + 1].x - a[j].x));
}
for (int i = 1; i <= n; i++) ans = min (ans, min (dp[i][i][0], dp[i][i][1]) + abs (b - a[i].x));
cout << ans;
考虑如果对一个人进行 DP,发现由于左右端点不固定,不很好转移,考虑对时间 DP。时间显然可以离散化到
设
枚举对距离最大的敌人的攻击时间
令
如果区间不完全包含某个敌人 DP 值为
for (int len = 1; len <= w; ++len) {
for (int l = 1, r, p = 0; (r = l + len - 1) <= w; ++l) {
for (int i = 1; i <= n; ++i)
if (l <= a[i].l && a[i].r <= r && (a[i].d > a[p].d)) p = i;
if (!p) {
f[l][r] = 0;
} else {
f[l][r] = 1E18;
for (int k = a[p].l; k <= a[p].r; ++k) Chmin(f[l][r], f[l][k - 1] + f[k + 1][r]);
f[l][r] += a[p].d;
}
p = 0; // 记得清空 QWQ
}
}
printf("%lld\n", f[1][w]);
根据数据范围,不难想到 DP 状态应该是
先考虑当没有反转区间的操作时如何转移。
设
加上翻转操作后,我们思考其本质。翻转一个子序列可以理解为交换某几对数字的位置,这样的话相当于如果
由于区间 DP 按照区间从小到大的顺序,故可以保证这样的翻转满足题目条件,所以这道题就结束了。
cin >> n;
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++)
for (int l = 1; l <= a[i]; l++)
for (int r = a[i]; r <= 50; r++) dp[i][i][l][r] = 1;
for (int len = 2; len <= n; len++) {
for (int l = 1; l <= n - len + 1; l++) {
int r = l + len - 1;
for (int lenn = 1; lenn <= 50; lenn++) {
for (int L = 1; L <= 50 - lenn + 1; L++) {
int R = L + lenn - 1;
dp[l][r][L][R] = max(dp[l][r][L + 1][R], dp[l][r][L][R - 1]);
dp[l][r][L][R] = max(dp[l][r][L][R], dp[l + 1][r][L][R] + (a[l] == L));
dp[l][r][L][R] = max(dp[l][r][L][R], dp[l][r - 1][L][R] + (a[r] == R));
dp[l][r][L][R] = max(dp[l][r][L][R], dp[l + 1][r - 1][L][R] + (a[l] == R) + (a[r] == L));
}
}
}
}
cout << dp[1][n][1][50];
考虑对于:
A B
C D
这样的节点,一定不会是
考虑设
答案为
转移式子:
输出方案可以在记录一个
inline double Dis(int i, int j) {
return sqrt((a[i].x - a[j].x) * (a[i].x - a[j].x) + (a[i].y - a[j].y) * (a[i].y - a[j].y));
}
void Dfs(int l, int r, int op) {
if (l == r) return printf(" %d", a[l].id), void();
if (op) printf(" %d", a[r].id), Dfs(l, r - 1, p[l][r][op]);
else printf(" %d", a[l].id), Dfs(l + 1, r, p[l][r][op]);
}
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> s[i].x >> s[i].y, s[i].id = i, ((s[i].y > maxy) && (maxy = s[i].y, k = i));
for (int i = 1; i <= n; ++i) a[(i - k + n) % n] = s[i]; a[n] = s[k];
for (int len = 2; len < n; ++len)
for (int l = 1, r; (r = l + len - 1) < n; ++l) {
f[l][r][0] = f[l][r][1] = 1E100;
if (f[l][r][0] > f[l + 1][r][0] + Dis(l + 1, l)) f[l][r][0] = f[l + 1][r][0] + Dis(l, l + 1), p[l][r][0] = 0;
if (f[l][r][0] > f[l + 1][r][1] + Dis(r, l)) f[l][r][0] = f[l + 1][r][1] + Dis(r, l), p[l][r][0] = 1;
if (f[l][r][1] > f[l][r - 1][0] + Dis(l, r)) f[l][r][1] = f[l][r - 1][0] + Dis(l, r), p[l][r][1] = 0;
if (f[l][r][1] > f[l][r - 1][1] + Dis(r - 1, r)) f[l][r][1] = f[l][r - 1][1] + Dis(r - 1, r), p[l][r][1] = 1;
}
cerr << min(f[1][n - 1][0] + Dis(1, n), f[1][n - 1][1] + Dis(n - 1, n)) << endl;
printf("%d", k), f[1][n - 1][0] + Dis(1, n) < f[1][n - 1][1] + Dis(n - 1, n) ? Dfs(1, n - 1, 0) : Dfs(1, n - 1, 1);
return 0;
}
根据题目要求,容易想到区间计数动态规划。
我们只考虑左括号,设
考虑转移,我们设 A
为一个合法括号串,它表示的区间是
(A)
,此时需满足
()A
,此时和上面类似,需满足
(A
,同时把 A
从中间断开插入右括号,设当前枚举的断点为
对于这些限制的快速判断我们可以维护一个矩阵,每条信息相当于矩阵上的一个点,这样对于前文的要求可以快速用二位前缀和的值的存在与否判断是否全部满足。
int ask(int l1, int r1, int l2, int r2) {
return sum[l2][r2] - sum[l1 - 1][r2] - sum[l2][r1 - 1] + sum[l1 - 1][r1 - 1];
}
void Main() {
n = read(), m = read(), ff = true;
for (int i = 0; i <= n; i++)
for (int j = 0; j <= n; j++) a[i][j] = sum[i][j] = dp[i][j] = 0;
for (int i = 1; i <= m; i++) {
int x = read(), y = read();
a[x][y] = 1;
if (x == y) ff = 0;
}
if (!ff) return puts("0");, void();
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
sum[i][j] = a[i][j] + sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1];
for (int i = 1; i <= n; i++) dp[i][i] = 1;
for (int len = 2; len <= n; len++)
for (int l = 1; l <= n - len + 1; l++) {
int r = l + len - 1;
if (!ask(l, l + 1, l, r)) (dp[l][r] += dp[l + 1][r]) %= mod;
if (!ask(l + 1, l, r, l)) (dp[l][r] += dp[l + 1][r]) %= mod;
for (int k = l + 1; k < r; k++)
if (!ask(l, l + 1, l, k) && !ask(k + 1, l, r, k))
dp[l][r] = (dp[l][r] + dp[l + 1][k] * dp[k + 1][r] % mod) % mod;
}
printf("%lld\n", dp[1][n]);
}
当你列出 DP 状态发现无法转移的时候,不妨添加状态。
设
设
则
转移:
枚举断点,合并两个区间。
找到
枚举
for (int i = 1; i <= n; i++) {
dp[i][i] = A;
for (int l = 1; l <= m; ++l)
for (int r = l; r <= m; ++r)
if (a[i] < l || a[i] > r) f[i][i][l][r] = A;
else f[i][i][l][r] = 0;
}
for (int len = 1; len < n; len++)
for (int i = 1, j = i + len; j <= n; ++i, ++j)
for (int l = 1; l <= m; ++l)
for (int r = l; r <= m; ++r) {
int p = 0, q = 0;
for (int k = i; k <= j; ++k) if (a[k] < l || a[k] > r) { p = k; break; }
for (int k = j; k >= i; --k) if (a[k] < l || a[k] > r) { q = k; break; }
if (p > 0 && q > 0) f[i][j][l][r] = dp[p][q];
for (int k = i; k < j; ++k)
f[i][j][l][r] = min(f[i][j][l][r], f[i][k][l][r] + f[k + 1][j][l][r]);
dp[i][j] = min(dp[i][j], f[i][j][l][r] + A + B * (tmp[r] - tmp[l]) * (tmp[r] - tmp[l]));
}
cout << dp[1][n];
考虑一个结论:一定存在一种最优方案使得使得任意一次染色的区间一定是完全包含之前某一次染色区间或者与之前某一次染色区间完全不交且不与之前所有染色区间相交。
简单证明,如果我们当前的染色方案与之前某一次相交,那么我们完全可以缩短当前染色区间使得不交。
我们发现这个性质能够把染色的过程抽象为类似线段树的结构,即线段与线段之间只存在包含和并列的关系,一个大区间的答案可以由其中的小区间合并而来,于是我们可以想到用区间 DP 解决这个问题。
这样我们设
完全包含。此时一定有 ABA
,它可以由 AB
直接转移,即第一次染色时可以向右端点多染一次。这里可能会有疑问,也许这种染色方式会不满足上文条件。但其实不要紧,因为就算不满足我们也可以通过前文的转化方式使其满足条件。等价于转移时不需要完全满足上文条件。
完全不相交。我们在这里其实只需要考虑紧挨的情况,即类似 AABB
,因为不紧挨的情况可以由多个紧挨的情况转移过来。所以只需要枚举断点
scanf("%s", s + 1), n = strlen(s + 1), memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i++) dp[i][i] = 1;
for (int len = 2; len <= n; len++) {
for (int l = 1; l <= n - len + 1; l++) {
int r = l + len - 1;
if (s[l] == s[r]) // 此时根据定义应有 dp[l][r] = dp[l][r - 1] = dp[l + 1][r],所以可以直接转移
dp[l][r] = min(dp[l][r - 1], dp[l + 1][r]);
else
for (int k = l; k <= r; k++) dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
}
cout << dp[1][n];
首先,题目上说让期望乘上
然后我们考虑在操作过后一个位置上的值相对于原来的值肯定是不降的,于是可以想到对每一个值
于是我们可以对每个区间都考虑一个 DP。设
转移的话显然可以从
回到题面,我们现在要解决的问题就是求每个数字最终大小为某个定值时的方案数。上述 DP 求的是每个区间最终值不大于某个定值时的方案数,那我们对于一个区间
这样对于每一个元素,我们只需要找到所有包含它的区间,再对于这个元素在该区间内可能变成的所有值都累加答案即可。根据定义,可以保证方案不会重复。
根据数据随机的条件,时间复杂度近似
但是还有更优的做法。我们发现,对于每一个值
具体的,在赋初值的时候我们枚举区间
inline int calc(int l, int r) {
return (l * (l - 1) / 2 % mod + (r - l + 1) * (r - l + 2) / 2 % mod + (n - r) * (n - r + 1) / 2 % mod) % mod;
}
int main() {
cin >> n >> q;
for (int i = 1; i <= n; i++) a[i] = read();
for (int l = 1; l <= n; l++) {
int ma = 0;
for (int r = l; r <= n; r++) {
ma = max(ma, a[r]);
if (ma >= min(a[l - 1], a[r + 1])) continue;
int num = min(a[l - 1], a[r + 1]);
if (l == 1 && r == n) num = 0;
dp[s1][l][r] = (ma - num + mod) % mod;
}
}
for (int l = 1; l <= n; l++) {
for (int r = l; r <= n; r++)
sum[s1][l][r] = (sum[s1][l - 1][r] + dp[s1][l][r] * (l - 1) % mod) % mod;
for (int r = n; r >= l; r--)
sumr[s1][l][r] = (sumr[s1][l][r + 1] + dp[s1][l][r] * (n - r) % mod) % mod;
}
for (int T = 1; T <= q; T++) {
swap(s1, s2);
for (int len = 1; len <= n; len++)
for (int l = 1, r; (r = l + len - 1) <= n; l++)
dp[s1][l][r] = ((dp[s2][l][r] * calc(l, r) % mod + sum[s2][l - 1][r]) % mod + sumr[s2][l][r + 1]) % mod;
for (int l = 1; l <= n; l++) {
for (int r = l; r <= n; r++)
sum[s1][l][r] = (sum[s1][l - 1][r] + dp[s1][l][r] * (l - 1) % mod) % mod;
for (int r = n; r >= l; r--)
sumr[s1][l][r] = (sumr[s1][l][r + 1] + dp[s1][l][r] * (n - r) % mod) % mod;
}
}
for (int i = 1; i <= n; i++) {
int ans = 0;
for (int l = 1; l <= i; l++)
for (int r = i; r <= n; r++) ans = (ans + dp[s1][l][r]) % mod;
printf("%d ", ans);
}
return 0;
}
考察一个数
条件 1. 是好维护的,考虑针对条件 2. 进行 DP:设
枚举断点
然后发现你会
因为你状态设的是设
然后你就得到了 废话
考虑
设
最后答案就是
代码就是模拟以上 DP,注意区间长度恒为偶数,记得赋初值。
n = read(); for (int i = 1; i <= n; ++i) a[i] = read();
for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j) f[i][j] = 1E9;
for (int len = 2; len <= n; len += 2)
for (int l = 1, r; (r = l + len - 1) <= n; ++l) {
if (((l & 1) != (a[l] & 1)) || (l < a[l])) continue;
if (len == 2 || f[l + 1][r - 1] <= (l - a[l]) / 2)
f[l][r] = (l - a[l]) / 2;
for (int k = l + 1; k <= r - 1; k += 2)
Chmin(f[l][r], max({(l - a[l]) / 2, f[l][k], f[k + 1][r] - (k - l + 1) / 2}));
}
for (int i = 1; i <= n; ++i) {
dp[i] = dp[i - 1];
for (int j = i - 1; j > 0; j -= 2)
if (f[j][i] <= dp[j - 1]) Chmax(dp[i], dp[j - 1] + (i - j + 1) / 2);
}
printf("%d\n", dp[n]);
<h4>简要题意</h4>
从一个序列两端取数放入栈中,栈中有两个数相同即可消去。求最后栈内元素最少值,和在此条件下栈最大空间最小值。
<h4>转移 1</h4>
设
考虑转移:
使
考虑枚举和它匹配的
变换至序列两端:
那么就需要取出
设
变换至序列一端:
同理需要取出
转移:
以上两种在代码中实现是可以令 j
使
同理分为变换至序列两端、变换至序列一端两类,转移类似。
<h4>转移 2</h4>
然后中间有个
使
枚举
找一个
即:消去内部的空间,消去外部的和这次空间,两者最大值。
枚举
使
其实
<h4>时间复杂度</h4>