题目归档 #1
目录
- [Luogu P6059] 纯粹容器
- [JSOI2015] Salesman
- [HNOI2007] 梦幻岛宝珠
[Luogu P6059] 纯粹容器
一道 Luogu 的个人公开赛题目,感觉这道题应该比较经典。
题意
- 有 \(n\) 个容器排成了一队,从左到右依次编号为 1∼n。第 \(i\) 个容器的强度为 \(a_i\) 且互不相同。为了挑选出最纯粹的容器,进行 \(n-1\) 轮操作,每轮操作中,等概率随机挑选两个 位置相邻 且 未被击倒 的容器,令它们进行决斗,在一次决斗中,强度较小的容器将会被击倒并移出队列。
- 显然最后留下的是强度最大的容器,但是,可怜的容器们很想知道自己能够活多久,于是,它们请你对每个容器求出它存活轮数的期望。答案对 \(998244353\) 取模。
- \(n \leq 50\)
解题
首先我们很快可以看出一个结论:显然容器存活时间只和 两边离他最近的强度大于他的容器 的位置有关。至于周围其他的容器是什么强度对答案没有影响。
然后开始想。一开始我以为直接推出公式算(一看就是期望 dp 做少了),于是开始从贡献的角度去考虑。先算出一个容器两边消完时间的期望再合并答案。
然而并不行。推了两个小时都没推出个所以然。而后看题解,原来就是期望 dp。
对于每个位置做一次 dp,大概如下:(令这个位置为 \(u\),两边离他最近的强度大于他的容器位置分别为 \(pre\) 和 \(nex\))
记 \(f[k][i][j]\) 表示第 \(k\) 轮 \(u\) 左侧直到 \(pre\) 还有 \(i\) 个容器完好,右侧则为 \(j\) 个容器。转移显然为 \(f[k][i][j]=f[k-1][i][j]\times\frac{n-k-i-j}{n-k} + f[k-1][i+1][j] \times \frac{i+1}{n-k} + f[k-1][i][j+1] \times \frac{j+1}{n-k}\)
注意当 \(pre = 0\)或者 \(nex = N+1\) (即某边没有强度大于他的容器)时需要特殊处理,体现为转移方程的分子要进行微调。详细见代码,注意取模要用到逆元。
程序
#include <iostream>
#include <cstring>
#include <cstdio>
#define Maxn 55
#define LL long long
using namespace std;
const LL MOD = 1ll * 998244353;
int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int N, a[Maxn], pre[Maxn], nex[Maxn];
LL f[Maxn][Maxn][Maxn], ans;
LL Pow(int a, int b) {
LL res = 1, base = a;
while(b) {
if(b & 1) res = res * base % MOD;
base = base * base % MOD;
b >>= 1;
}
return res;
}
int main() {
N = read();
for(int i = 1; i <= N; ++i) a[i] = read();
for(int i = 1; i <= N; ++i) {
pre[i] = i, nex[i] = N - i + 1;
for(int j = i - 1; j >= 1; --j) if(a[i] < a[j]) {pre[i] = i - j; break;}
for(int j = i + 1; j <= N; ++j) if(a[i] < a[j]) {nex[i] = j - i; break;}
}
for(int u = 1; u <= N; ++u) {
memset(f, 0, sizeof(f));
f[0][pre[u]][nex[u]] = 1;
ans = 0;
for(int k = 1; k < N; ++k) {
LL inv = Pow(N - k, MOD - 2) % MOD;
for(int i = 1; i <= pre[u]; ++i)
for(int j = 1; j <= nex[u]; ++j) {
if(i + j > N - k - 1 + (pre[u] == u) + (nex[u] == N - u + 1)) continue;
if(pre[u] == u) f[k][i][j] = f[k - 1][i + 1][j] * i % MOD;
else f[k][i][j] = f[k - 1][i + 1][j] * (i + 1) % MOD;
if(nex[u] == N - u + 1) f[k][i][j] += f[k - 1][i][j + 1] * j % MOD;
else f[k][i][j] += f[k - 1][i][j + 1] * (j + 1) % MOD;
f[k][i][j] += f[k - 1][i][j] * (N - k - i - j + (pre[u] == u) + (nex[u] == N - u + 1)) % MOD;
f[k][i][j] = f[k][i][j] * inv % MOD;
ans += f[k][i][j]; ans %= MOD;
// cout << u << " " << k << " " << i << " " << j << " " << f[k][i][j] << endl;
}
}
printf("%lld ", ans);
}
return 0;
}
[JSOI2015] Salesman
一道比较考察细节处理能力的 dp 题目。
题意
- 一个 \(N\) 个点的树。某个点有一个可正可负的点权以及经过次数的限制。现在从 \(1\) 号点出发,可以自行规划路径,最终回到 \(1\) 号点。你的得分为途中经过所有点的点权之和。求得分最大值。特别地,\(1\) 号点可经过无数次,且其点权为 \(0\) 。
- Task 1:得分的最大值?
- Task 2:使得分最大化的路径是否唯一存在?(Yes/No)
- \(n \leq 10^5\),所有点经过次数限制均一定不小于 \(2\)。
解题
这道题要 easy 一些了。显然我们可以得出一个规律:如果你只能经过一个点 \(x\) 次,那么你可以选择这个点的 \(x-1\) 个子树经过。至此「树形 DP」和「贪心」都很明显了。对于每个点,贪心地选取前 \(x-1\) 个儿子的 dp 值并计入该节点的 dp 值。
至于 Task 2,对于某个节点,如果第 \(x-1\) 大的子树的 dp 值与第 \(x\) 大的子树的 dp 值相等,那么路径就不唯一,因为选两棵子树走下去都可以。然后把信息更新到其父节点即可。
程序
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#define Maxn 100010
#define Maxm 100010
using namespace std;
int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int N, val[Maxn], lim[Maxn];
struct Edge {
int next, to;
}
edge[Maxm * 2];
int head[Maxn], edge_num;
void add_edge(int from, int to) {
edge[++edge_num].next = head[from];
edge[edge_num].to = to;
head[from] = edge_num;
}
struct Node {
int data;
bool uni;
}
node[Maxn];
int tot, dp[Maxn], flag[Maxn];
int comp(Node x, Node y) {
return x.data > y.data;
}
void dfs(int u, int f) {
int tot = 0;
dp[u] = val[u];
for(int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
if(v == f) continue;
dfs(v, u);
}
for(int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
if(v == f) continue;
node[++tot].data = dp[v];
node[tot].uni = flag[v];
}
sort(node + 1, node + tot + 1, comp);
int rec_i = 0;
for(int i = 1; i <= lim[u] && i <= tot && node[i].data > 0; ++i) {
dp[u] += node[i].data;
flag[u] |= node[i].uni;
rec_i = i;
}
if(rec_i != tot && (node[rec_i].data == node[rec_i + 1].data || (node[rec_i + 1].data == 0 && rec_i != lim[u] - 1)))
flag[u] = 1;
}
int main() {
N = read();
val[1] = 0, lim[1] = N;
for(int i = 2; i <= N; ++i) val[i] = read();
for(int i = 2; i <= N; ++i) lim[i] = read() - 1;
int u, v;
for(int i = 1; i < N; ++i) {
u = read(); v = read();
add_edge(u, v);
add_edge(v, u);
}
dfs(1, 0);
// for(int i = 1; i <= N; ++i) cout << dp[i] << endl;
cout << dp[1] << endl;
if(flag[1]) cout << "solution is not unique";
else cout << "solution is unique";
return 0;
}
[HNOI2007] 梦幻岛宝珠
很有意思的一个背包问题变种。细节堪称工艺品级别的动态规划,和二进制联系紧密但并非如同多重背包问题的二进制分组。
题意
- 同「01背包问题」,但是在数据范围上有变化:
- 总容量 \(W \leq 2^{30}\)。保证单个物品的体积 \(w_i\) 可以拆分为 \(a \times 2^b\) 的形式,其中 \(a \leq 10, b \leq 30\)。
解题
很明显这道题要用到如上性质,大多数人第一反应肯定是二进制分组。没错,但是我们该如何分组呢?
首先我们一定可以把每个 \(w_i\) 拆分为 \(a \times 2^b\) 的形式,且使得 \(a\) 为奇数。如果忽视掉 \(b\),只考虑 \(a\),显然这个「01背包」大家都会做。
那么何不先对于每个物品根据其体积拆分后的 \(b\) 来分组呢?显然最多只有 \(31\) 组。对这 \(31\) 组暴力完成「01背包」,我们可以得到一个数组 \(f[i][j]\)。表示如果仅使用 \(b=i\) 的物品,在容积为 \(j \times 2^i\) 下的最大价值。
显然现在的问题就是如何合并不同的 \(i\)(说 \(b\) 其实也可以)。首先我们现在要改变 \(f[i][j]\) 的定义——现在 \(f[i][j]\) 表示在 \(2^0 \sim 2^i\) 这几个组内,容量为 \(j \times 2^i + q\) 的最大价值。其中 \(q\) 代表二进制表示 \(W\) 的 \(0\sim i-1\) 这几位的部分。例如 \(W=01001\underline{01101}\),当 \(i=5,j=3\)时,\(q\) 即为下划线部分,容量为 \(1101101\)。
接着给出状态转移方程:
\(f[i][j] = max(f[i][j], f[i][j - k] + f[i - 1][min(val[i - 1], k * 2 + ((W >> (i - 1)) & 1))])\)。
其中,\(W\) 即为总容积,\(val[i]\) 表示最初我们把每个 \(w_i\) 拆分为 \(a \times 2^b\) 的形式后,\(2^i\) 的系数之和(即若干个 \(a\) 的和)。
其中 \(i\) 是最外层循环,\(j\) 是中层循环(并且采用 倒序循环),\(k\) 是内层循环,从 \(1\) 循环到 \(j\)。\(k\) 的存在实际上有一种「借位」的思想。肉眼可见,从 \(f[i][j]\) 中借走了个 \(k\) ,在 \(f[i-1][...]\) 中自然要加上 \(k \times 2\)。而后面的 \(((W >> (i - 1)) & 1)\) 则是为了判断第 \(i-1\) 位是否为 \(1\)。为 \(1\) 则还要再加个 \(1\),因为这是原本在 \(f[i-1][...]\) 中就存在的。
由于中层倒序循环,\(f[i][j-k]\) 尚且还在旧定义中,即 \(2^0 \sim 2^i\) 这几个组内,容量为 \(j \times 2^i + q\) 的最大价值。而后面的 \(f[i-1][...]\) 已经是新定义了,所以它就包含了它本身的旧定义以及之前所说的 \(q\) 那一部分,凑起来刚好是 \(f[i][j]\)。中途对 \(val[i-1]\) 取 \(\min\),是为了确认范围的合法。
大概就是这些。细节请移步代码。
程序
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#define Maxn 110
#define Maxa 12
#define Maxb 36
using namespace std;
int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int N, W, tot, f[Maxb][Maxn * Maxa], val[Maxb];
vector <int> C[Maxb];
vector <int> V[Maxb];
void init() {
tot = 0;
memset(f, 0, sizeof(f));
memset(val, 0, sizeof(val));
memset(C, 0, sizeof(C));
memset(V, 0, sizeof(V));
}
int main() {
while(true) {
N = read(); W = read();
if(N == -1 && W == -1) break;
init();
int w, v;
for(int i = 1; i <= N; ++i) {
w = read(); v = read();
int x = 0;
while(!(w & 1)) ++x, w >>= 1;
val[x] += w; tot = max(tot, x);
// cout << "x " << x << "w " << w << endl;
C[x].push_back(w);
V[x].push_back(v);
}
for(int x = 0; x <= tot; ++x) {
for(int i = 0; i < C[x].size(); ++i) {
for(int j = val[x]; j >= C[x][i]; --j) {
f[x][j] = max(f[x][j], f[x][j - C[x][i]] + V[x][i]);
}
}
}
tot = 0;
while(W >> tot) ++tot;
--tot;
for(int x = 1; x <= tot; ++x) {
val[x] += (val[x - 1] + 1) / 2;
for(int j = val[x]; j >= 0; --j)
for(int k = 0; k <= j; ++k)
f[x][j] = max(f[x][j], f[x][j - k] + f[x - 1][min(val[x - 1], k * 2 + ((W >> (x - 1)) & 1))]);
}
printf("%d\n", f[tot][1]);
}
return 0;
}

浙公网安备 33010602011771号