闲话
单纯型优化dp怎么学?
怎么今天的题都是dp啊
我们看今天的simulation都考了点啥
送温暖水题
送温暖水题
看不懂题解但是能拿网络流写一个样例都过不去的AC代码的题
看不懂题解但是能玄学剪枝剪到卡不住的题
关于译名这件事,我个人认为只要所有人都能明白你在指代什么,这个名字就无可指摘。
我也曾作为一名翻译活动,译名这种东西本来就是从很多种无所谓更好的名字中找出一个顺眼的名称作为指代。
凭译者自己大抵很多时候是无法确实地抵达作者内心的。当然如果译者是作者本人的话另说。
所以译名这种东西还是随意使用的好,别去指摘“译名的精准性”。如果不是译者的话最好不要指摘。
译者另说。
但是我是不会在乎别人拿差不多甚至更顺的称呼来指代我的作品的。
特殊情况另说。
昨天和jjdw关于译名对线,于是有了这段。
今天simu.打了三个前向星
所以在注释里打了“今天一定要放Aster的歌词”
于是放一下。
作り物のワイヤーフレーム
特別なプレゼンテイション
永遠の甘い夢
人は何にも見えなくなった
仕事を持ってママと出会ってお家を買って心を買って
真っ白な身体に植え付けられただけ
杂题
考虑一个长度为偶数 \(n\) 的序列 \(a_1, a_2, \dots, a_n\),我们称这个序列为好的,当且仅当存在 \(a_1, a_2, \dots, a_n\) 的一个划分 \(U=\{ a_{i_1}, a_{i_2}, \dots, a_{i_{n/2}} \}, V=\{ a_{j_1}, a_{j_2}, \dots, a_{j_{n/2}} \}=\{ a_1, a_2, \dots, a_n \}-U\),且 \(i_1<i_2< \dots <i_{n/2}, a_{i_1}<a_{i_2}< \dots <a_{i_{n/2}}, j_1<j_2< \dots <j_{n/2}, a_{j_1}<a_{j_2}< \dots <a_{j_{n/2}}\)。
现在的问题是,针对给出的若干序列,请你判断它们是否是好的序列。
\(T \le 25, n \le 2000, a_i \le 10^9\)
dp题。
这题和几天(?)前一道题类似。这篇社论里T1。
然后相同的思路,dp维护子序列末尾元素。
我们设 \(f[i][j]\) 表示目前已经决策到 \(i\),当前两个序列中存在 \(a_i\) 的序列的长度为 \(j\),不存在 \(a_i\) 的序列末尾元素的大小为 \(f[i][j]\)。于是不存在 \(a_i\) 的序列的长度就是 \((i-j)\)。
初始化所有状态为 \(+\inf\),\(f[1][1]\) 为 \(-\inf\)。
然后考虑转移。
- \(a_i < a_{i+1}\),这时可以将 \(a_i\) 接在 \(a_j\) 后面,\(f[i][j]\) 对 \(f[i+1][j+1]\) 做贡献。
- \(a_i > f_{i,j}\),这时可以将 \(a_i\) 接在另一个序列后面,\(a_i\) 对 \(f[i+1][i-j+1]\) 做贡献。
然后就没了。
实现蛮简单的。
code
#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;
#define rep(i,a,b) for (register int i = (a), i##_ = (b) + 1; i < i##_; ++i)
#define pre(i,a,b) for (register int i = (a), i##_ = (b) - 1; i > i##_; --i)
const int N = 2e3 + 10;
int T, n, a[N], f[N][N];
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> T;
while (T--) {
cin >> n;
rep(i,1,n) cin >> a[i];
rep(i,1,n) rep(j,1,n>>1) f[i][j] = 0x3f3f3f3f;
f[1][1] = -0x3f3f3f3f;
rep(i,1,n-1) rep(j,1,min(n>>1,i)) {
if(a[i] < a[i + 1]) f[i + 1][j + 1] = min(f[i][j], f[i + 1][j + 1]);
if(a[i + 1] > f[i][j]) f[i + 1][i - j + 1] = min(a[i], f[i + 1][i - j + 1]);
} if (f[n][n>>1] == 0x3f3f3f3f) cout << "No!\n";
else cout << "Yes!\n";
}
}
给定一个 \(1\) 到 \(N\) 的排列 \(P_{1\ldots N}\)。定义波动强度等于相邻两项的差的绝对值的和,即:
\[L = | P_2 – P_1 | + | P_3 – P_2 | +\ldots + | P_N – P_{N-1} | \]给定 \(N\) 和 \(M\),问:随机一个 \(1\ldots N\) 的排列,它的波动强度不小于 \(M\) 的概率有多大? 答案请保留小数点后 \(K\) 位输出,四舍五入。
\(N \le 100, M \le 2^32, K \le 30\)。
连续段dp题。
你看这个 \(M\) 那么大,上界根本取不到。事实上,\(M = O(n^2)\)。实现上考虑dp \([-5000,+5000]\) 的范围就足够了。
然后我们把绝对值号拆开,得到结果时讨论哪边较大即可。
套路地,我们考虑从 \(1\) 到 \(N\) 顺序插入的情况。
首先需要特殊注意 \(P_1\) 和 \(P_N\) 的两个位置,它们只会和一边的值作贡献。因此我们特殊讨论一下这两个部分。然后开始分类讨论插入 \(i\) 的情况:
- 一边与边界相连,一边为空:增加一个连续段。后来这个位置一定会且仅会和空的一侧新插入元素做出 \(-i\) 的贡献。系数为 \([未插入元素的边界数]\)。
- 一边与边界相连,一边为连续段:连续段数不变,立即作出 \(-i\) 的贡献。系数为 \([未插入元素的边界数]\)。
- 连接两段连续段:连续段数减一,立即作出 \(2i\) 的贡献。系数为 \([连续段数] - 1\)。
- 插在一段连续段一侧:连续段数不变,立即作出 \(i\) 的贡献,并会在之后和另一侧元素作出 \(-i\) 的贡献,因此总贡献为 \(0\)。系数为 \(2[连续段数] - [插入元素的边界数]\)。
- 自成一段:连续段数+1,在之后和两侧元素作出总 \(-2i\) 的贡献。系数为 \([连续段数] + 1 - [插入元素的边界数]\)。
讨论体现了特殊讨论边界的重要性。
然后实现一下,我们设 \(f[i][j][k][l]\) 为插入 \(i\),有 \(j\) 个连续段,当前总价值为 \(k\),插入元素的边界数为 \(l\) 的方案数。
按讨论dp即可。
注意在 \(k\le 8\) 时需要用double,用float128会TLE。
注意在 \(k> 8\) 时需要用float128,用double会WA。
注意除总方案数时别一次乘进去,要不然会丢精度。
code
#include <bits/stdc++.h>
#define rep(i, a, b) for (register int(i) = (a); (i) <= (b); ++(i))
#define pre(i, a, b) for (register int(i) = (a); (i) >= (b); --(i))
using namespace std;
int n, m, k;
namespace use_float128 {
__float128 f[2][101][10001][3], tmp = 1.0, tot;
void print(__float128 ans, int k) {
int d = int(ans); cout << d << '.';
while (k--) {
ans = (ans - 1.0 * d) * 10.0;
if (k == 0) ans += 0.5;
d = int(ans), cout << d;
}
}
void main() {
f[0][0][5000][0] = 1.0;
rep(i,2,min(n,20)) f[0][0][5000][0] /= tmp * i;
rep(i,1,n) {
int ptr = i & 1, ztr = ptr ^ 1;
memset(f[ptr], 0, sizeof f[ptr]);
rep(j,0,i) rep(k,0,10000) rep(l,0,2) {
tot = f[ztr][j][k][l];
if (tot <= 1e-30) continue;
if (l ^ 2) {
if (j > 0) f[ptr][j][k+i][l+1] += tot * (2 - l);
f[ptr][j+1][k-i][l+1] += tot * (2 - l);
}
if (j > 0) f[ptr][j][k][l] += tot * (2 * j - l);
f[ptr][j+1][k-i*2][l] += tot * (j + 1 - l);
if (j > 1) f[ptr][j-1][k+2*i][l] += tot * (j - 1);
}
} tot = 0;
rep(i,m+5000, 10000) tot += f[n & 1][1][i][2];
rep(i,21,n) tot /= tmp * i;
print(tot, k);
}
} ;
namespace use_double {
double f[2][101][10001][3], tmp = 1.0, tot;
void print(double ans, int k) {
int d = int(ans); cout << d << '.';
while (k--) {
ans = (ans - 1.0 * d) * 10.0;
if (k == 0) ans += 0.5;
d = int(ans), cout << d;
}
}
void main() {
f[0][0][5000][0] = 1.0;
rep(i,2,min(n,20)) f[0][0][5000][0] /= tmp * i;
rep(i,1,n) {
int ptr = i & 1, ztr = ptr ^ 1;
memset(f[ptr], 0, sizeof f[ptr]);
rep(j,0,i) rep(k,0,10000) rep(l,0,2) {
tot = f[ztr][j][k][l];
if (tot <= 1e-30) continue;
if (l ^ 2) {
if (j > 0) f[ptr][j][k+i][l+1] += tot * (2 - l);
f[ptr][j+1][k-i][l+1] += tot * (2 - l);
}
if (j > 0) f[ptr][j][k][l] += tot * (2 * j - l);
f[ptr][j+1][k-i*2][l] += tot * (j + 1 - l);
if (j > 1) f[ptr][j-1][k+2*i][l] += tot * (j - 1);
}
} tot = 0;
rep(i,m+5000, 10000) tot += f[n & 1][1][i][2];
rep(i,21,n) tot /= tmp * i;
print(tot, k);
}
} ;
int main() {
cin >> n >> m >> k;
if (k <= 8) use_double :: main();
else use_float128 :: main();
return 0;
}