一些简单的dp
\(dp\)
自古不会 \(dp\) ,只会瞎写。
#181. 【UR #12】密码锁
题目大意
一张 \(n\) 个点的完全图,每条边有一定的概率随机定向,其中只有 \(m\) 条特殊的边的概率不为 \(0.5\) ,求随机定向后形成的图的强连通分量的期望。
\(1 \le n \le 38, 0 \le m \le 19\) 。
题解
首先不难发现这是一张竞赛图,那么缩点后会形成一条链,链中的每个前缀都满足前缀之外的点没有边连向自己,那么问题被转化为求图中期望有多少点集 \(S\) 只有出边,没有入边,根据期望的线性性,只要求出所有点集只有出边的概率求和即可。。
直接枚举点集显然不行,由于大部分边概率均为 \(0.5\) ,考虑求出同一大小的所有点集特殊边均为出边的概率和,设点集大小为 \(x\) ,则只要在计算时将特殊边的概率变为原来的两倍,最后乘上一个 \(0.5^{x(n-x)}\) 即可求出这个大小的点集只有出边的概率。
接下来考虑如何求同一大小的点集特殊边向外概率和。不难发现只有通过特殊边联通的点中,一个点属于 \(S\) ,另一个点不属于 \(S\) 时特殊边有贡献(这不是废话),那么可以考虑求出所有通过特殊边联通的连通块,这样最大的连通块大小不超过 \(m + 1\) ,于是对于每个这样的连通块,枚举所有点集,求特殊边向外的概率,再通过背包合并起来即可。
时间复杂度 \(O(2^{m+1}n)\) 具体细节看代码吧。。。
code
#include <bits/stdc++.h>
#define fi first
#define se second
const int N = 40, mod = 998244353;
int read(void) {
int f = 1, x = 0; char ch = getchar();
while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
return f * x;
}
using std::pair;
using std::make_pair;
int qp(int n, int m = mod - 2) {
int res = 1;
while(m) {
if(m & 1) res = 1ll * res * n % mod;
m >>= 1, n = 1ll * n * n % mod;
} return res;
}
int n, m;
int vis[N], g[N], f[N], num[1 << 20], rev[N], id[N], ind;
std::vector<pair<int, int> > l[N];
void dfs(int u, int x) {
vis[u] = x, rev[id[u] = ind++] = u;
for(pair<int, int> v : l[u])
if(!vis[v.fi]) dfs(v.fi, x);
}
signed main(void) {
n = read(), m = read(); int inv1 = qp(10000);
for(int i = 1; i < 1 << 20; ++i) num[i] = num[i - (i & -i)] + 1;
for(int i = 1, x, y, z; i <= m; ++i) {
x = read(), y = read(), z = 1ll * inv1 * read() % mod;
l[x].push_back(make_pair(y, z)), l[y].push_back(make_pair(x, mod + 1 - z));
}
f[0] = 1;
for(int i = 1; i <= n; ++i) if(!vis[i]) {
ind = 0, dfs(i, i);
std::swap(g, f), memset(f, 0, sizeof f);
for(int s = 0, val = 1; s < 1 << ind; ++s, val = 1) {
for(int j = 0; j < ind; ++j) if((s >> j) & 1)
for(pair<int, int> v : l[rev[j]])
if(vis[v.fi] == i && !((s >> id[v.fi]) & 1))
val = 2ll * val * v.se % mod;
for(int j = num[s]; j <= n; ++j)
(f[j] += 1ll * g[j - num[s]] * val % mod) %= mod;
}
} int inv2 = qp(2), res = 0;
for(int i = 1; i <= n; ++i)
(res += 1ll * f[i] * qp(inv2, i * (n - i)) % mod) %= mod;
printf("%lld\n", 1ll * res * qp(10000, n * (n - 1) % mod) % mod);
return 0;
}
#370. 【UR #17】滑稽树上滑稽果
题目大意
给定序列 \(a\) ,要求生成一棵树,每个点 \(u\) 的权值为从根到当前节点的 \(a_u\) 的前缀 \(and\) ,求最小权值和。
题解
首先不难发现最终生成的一棵树实际上是一条链,证明很简单,若一个点的父亲是 \(u\) 的祖先,那么让它认 \(u\) 作父一定不劣。
考虑如何生成这样的一条链。首先找到在所有数种都存在的公共位,这几位会一直存在,那么从所有数种把它们删去,最后加上即可。这样链尾的权值一定会变成 \(0\) ,那么不妨设 \(f_i\) 表示当前链尾的数是 \(i\) 时,链的权值和是多少,枚举子集转移即可,最终答案便是 \(f_0\) 加上公共位的贡献。
可能写的不清楚。。。细节看代码把。。。
code
#include <bits/stdc++.h>
#define int long long
const int N = 2e6 + 10, all = (1 << 18) - 1;
int read(void) {
int f = 1, x = 0; char ch = getchar();
while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
return f * x;
}
int n, a[N], f[N];
bool vis[N];
signed main(void){
n = read(); int con = all;
for(int i = 1; i <= n; ++i) con &= (a[i] = read());
for(int i = 1; i <= n; ++i) a[i] ^= con;
for(int i = 1; i <= n; ++i) vis[all ^ a[i]] = 1;
for(int i = 1; i < all; i <<= 1)
for(int j = 0; j < all; j += i << 1)
for(int k = 0; k < i; ++k)
vis[j + k] |= vis[i + j + k];
memset(f, 0x3f, sizeof f);
for(int i = 1; i <= n; ++i) f[a[i]] = a[i];
for(int i = all; i >= 1; --i) if(f[i] < f[0]){
for(int j = i; j; j = (j - 1) & i) if(vis[j])
f[i ^ j] = std::min(f[i ^ j], f[i] + (i ^ j));
}
printf("%lld\n", con * n + f[0]);
return 0;
}
CF1342F Make It Ascending
题目大意
给定序列 \(a\) ,每次可以选择 \(i, j\) ,将 \(a_i\) 加上 \(a_j\) 并删除 \(a_j\) ,求最少多少次操作可以使序列变成升序,输出方案。
题解
思路并不难,但是细节较多,不好写。
不难想到状压,设 \(f_{i,s,p}\) 表示已经有 \(i\) 个数完成合并完且排成升序,能选的集合为 \(s\) ,且当前数的位置为 \(p\) 时,第 \(i\) 个数最小是多多少,每次枚举 \(s\) 的一个子集,作为新的一个数,放在序列最后面,因为要输出方案,需要记录当前状态是从哪转移过来的。当然也可以用主席树在线回答。
细节貌似比较多。。。
code
#include <bits/stdc++.h>
const int N = 16, M = (1 << N) + 10, INF = 0x3f3f3f3f;
int read(void) {
int f = 1, x = 0; char ch = getchar();
while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
return f * x;
}
#define lb(x) (x & -x)
struct node {
int i, p, s;
node (int I = 0, int P = 0, int S = 0)
{ i = I, p = P, s = S; }
};
int T, n, a[N], id[N];
int f[N][N][M], ctz[M], num[M], sum[M];
node tr[N][N][M];
signed main(void) {
T = read();
for(int i = 1; i < 1 << N; ++i) num[i] = num[i - lb(i)] + 1;
for(int i = 1; i < N; ++i) ctz[1 << i] = i;
for(int i = 1; i < 1 << N; ++i) if(!ctz[i]) ctz[i] = ctz[lb(i)];
while(T--) {
n = read(); int al = (1 << n) - 1;
for(int i = 0; i < n; ++i) a[i] = read();
for(int i = 0; i < n; ++i) sum[1 << i] = a[i];
for(int i = 1; i < 1 << n; ++i) if(num[i] > 1)
sum[i] = sum[i - lb(i)] + sum[lb(i)];
for(int i = 0; i <= n; ++i)
for(int j = 0; j <= n; ++j)
for(int s = 0; s < 1 << n; ++s)
f[i][j][s] = INF;
f[0][0][0] = 0;
for(int i = 0; i < n; ++i) for(int p = 0; p < n; ++p)
for(int s = 0; s < 1 << n; ++s) if(f[i][p][s] ^ INF)
for(int t = al ^ s; t; t = (t - 1) & (al ^ s))
if(f[i][p][s] < sum[t] && (t >> p)) {
int P = p + 1 + ctz[t >> p], S = s | t;
if(f[i + 1][P][S] >= sum[t]) {
f[i + 1][P][S] = sum[t];
tr[i + 1][P][S] = node(i, p, s);
}
}
node ans = node(0, 0, 0);
for(int i = n; i; --i) {
for(int p = 1; p <= n; ++p) if(f[i][p][al] ^ INF)
{ ans = node(i, p, al); break; }
if(ans.i) break;
}
printf("%d\n", n - ans.i);
for(int i = 1; i <= n; ++i) id[i] = i;
while(ans.i) {
node pre = tr[ans.i][ans.p][ans.s];
int S = ans.s ^ pre.s;
for(int j = n - 1; ~j; --j) if((j + 1)^ ans.p) {
if((S >> j) & 1) {
printf("%d %d\n", id[j + 1], id[ans.p]);
for(int k = j + 1; k <= n; ++k) --id[k];
}
}
ans = pre;
}
}
return 0;
}
CF1295F Good Contest P3643 [APIO2016]划艇
这两题居然一摸一样。。。
题目大意
一个 \(n\) 个点的序列,第 \(i\) 个点的权值在 \(l_i, r_i\) 之间,每个点可以选或不选,求有多少方案使所有得选的数组成的序列严格递增,答案对 \(10^9 + 7\) 取模。
\(2 \le n \le 500, 1 \le l_i \le r_i \le 10^9\)
题解
首先不难写出建立在值域上的 \(dp\) ,设 \(f_{i, j}\) 表示前 \(i\) 个点,第 \(i\) 个点选 \(j\) 时的方案数,然而值域很大,显然不能直接 \(dp\) ,此时注意到 \(n\) 很小,只有 \(500\) ,那么所有 \(l, r\) ,至多也只有 \(1000\) 种取值,这些取值将值域分成 \(O(n)\) 个区间,对于一个区间内选择一些数组成一个递增序列显然可以通过组合数来求,那么可以对于值域上每个区间进行一次 \(dp\) ,把这个区间内的数选进去。
设这个区间的长度为 \(len\) ,则 \(x\) 个数,每个数可选可不选,选的数组成严格上升序列的方案为 \(\binom{len + x}{x}\) 。设 \(f_{i,j}\) 表示选的数在前 \(i\) 个区间内,序列前 \(j\) 项的方案数,可以通过倒叙枚举 \(j\) 省掉第一维。
code
#include <bits/stdc++.h>
const int N = 510, mod = 1e9 + 7;
int read(void) {
int f = 1, x = 0; char ch = getchar();
while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
return f * x;
}
int n, f[N], a[N], b[N], c[N << 1], inv[N];
signed main(void) {
n = read(); int cnt = 0;
for(int i = 1; i <= n; ++i)
c[i] = a[i] = read(), c[n + i] = b[i] = read() + 1;
std::sort(c + 1, c + 1 + n + n);
cnt = std::unique(c + 1, c + 1 + n + n) - c - 1;
inv[0] = inv[1] = 1, f[0] = 1;
for(int i = 2; i < N; ++i) inv[i] = 1ll * (mod - mod / i) * inv[mod % i] % mod;
for(int i = 1; i < cnt; ++i) {
int len = c[i + 1] - c[i];
for(int j = n; j; --j) if(a[j] <= c[i] && b[j] >= c[i + 1]) {
int C = len, num = 1;
for(int k = j - 1; ~k; --k) {
(f[j] += 1ll * C * f[k] % mod) %= mod;
if(a[k] <= c[i] && b[k] >= c[i + 1])
++num, C = 1ll * C * (len + num - 1) % mod * inv[num] % mod;
}
}
}
int res = 0;
for(int i = 1; i <= n; ++i) (res += f[i]) %= mod;
printf("%d\n", res);
return 0;
}
[AGC016F] Games on DAG
题目大意
给定 \(n\) 个点 \(m\) 条边的 \(DAG\) ,求有多少种边的集合满足一号节点的 \(SG\) 至不等于二号节点的 \(SG\) 值。
题解
直接算很难算,考虑用所有的情况减去 \(SG(1) = SG(2)\) 的情况。
根据 \(SG\) 函数的定义,考虑 \(SG\) 相等的一个点集又哪些性质。
\(SG\) 函数为所能到达点 \(SG\) 的 \(mex\) ,那么这个点集的内部一定没有连边,且对于任何 \(SG\) 小于它的点集,至少有一条连边。考虑状压,设 \(f_s\) 表示点集为 \(s\) ,有多少种方案。考虑如何转移,若每次枚举 \(SG\) 值更大的点集,那么很难得知它的连边情况,因此考虑枚举 \(SG\) 值更小的点集,每个点都得像这个点集种至少一个点连边,点集内部不能有边,其余便随便连即可。
code
#include <bits/stdc++.h>
#define lb(x) (x & -x)
const int N = 15, mod = 1e9 + 7;
int read(void) {
int f = 1, x = 0; char ch = getchar();
while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
return f * x;
}
int qp(int n, int m = mod - 2) {
int res = 1;
while(m) {
if(m & 1) res = 1ll * res * n % mod;
m >>= 1, n = 1ll * n * n % mod;
} return res;
}
int n, m, to[N], num[1 << N], bet[N][1 << N];
std::vector<int> l[N];
int f[1 << N], g[1 << N];
signed main(void) {
n = read(), m = read();
for(int i = 1; i < 1 << n; ++i) num[i] = num[i - lb(i)] + 1;
for(int i = 1, x, y; i <= m; ++i) {
x = read() - 1, y = read() - 1;
l[x].push_back(y), to[x] |= 1 << y;
}
for(int i = 0; i < n; ++i)
for(int s = 1; s < 1 << n; ++s)
bet[i][s] = num[to[i] & s];
for(int s = 3; s < 1 << n; s += 4) {
f[s] = 1;
for(int t = s & (s - 1); t; (--t) &= s) if((t & 1) == ((t >> 1) & 1)) {
if(t & 1) {
int val = 1;
for(int i = 0; i < n; ++i) {
if((t >> i) & 1) val = ((1ll << bet[i][s ^ t]) - 1) * val % mod;
else if((s >> i) & 1) val = (1ll << bet[i][t]) * val % mod;
} (f[s] += 1ll * f[t] * val % mod) %= mod;
} else {
int val = 1;
for(int i = 0; i < n; ++i) {
if((t >> i) & 1) val = ((1ll << bet[i][s ^ t]) - 1) * val % mod;
if((s >> i) & 1) val = (1ll << bet[i][t]) * val % mod;
} (f[s] += val) %= mod;
}
}
}
printf("%d\n", (qp(2, m) - f[(1 << n) - 1] + mod) % mod);
return 0;
}
CF762F Tree nesting
题目大意
给定两棵树 \(S, T\) 求 \(S\) 有多少联通子图和 \(T\) 同构。
\(1 \le |S| \le 1000, 1 \le |T| \le 12\)
题解
这不是WCR最喜欢切的NPC问题吗。。。
这显然是一个 \(NPC\) 问题,所以直接考虑可以接受的指数复杂度做法就好了,注意到 \(1 \le |T| \le 12\) ,不妨从这里入手,考虑状压。
直接状压整棵树难以实现匹配和转移,因此考虑状压儿子节点,设 \(dp\) 状态 \(f_{u, v, s}\) 为树 \(T\) 上的 \(u\) 子树匹配到了 \(S\) 树上的 \(v\) 节点, 其中树 \(T\) 上所选的儿子状态为集合 \(s\) 时的方案数,每次枚举树 \(T\) 上 \(v\) 的一个儿子节点,与 \(S\) 上 \(u\) 的所有儿子节点分别匹配,再状压转移即可。
钦定 \(T\) 的根为 \(1\) 枚举每个点作为 \(S\) 的根,这样答案即为 \(\sum_{i = 1}^{|S|} f_{1, i, U}\) 其中 \(U\) 为全集。
然而这样 \(dp\) 算出的是 \(T\) 同构 \(S\) 一个子集的方案数,而不是 \(S\) 种与 \(T\) 同构的联通子图数量,问题在于若 \(S\) 中一个子图有多种方法与 \(T\) 同构,那么这个子图会被算多次,这样每个子图都被多算了,不难发现被多算的次数实际上就是 \(T\) 与它本身同构的次数,再求一次同构即可。
code
#include <bits/stdc++.h>
#define lb(x) (x & -x)
const int N = 1010, M = 12, mod = 1e9 + 7;
int read(void) {
int f = 1, x = 0; char ch = getchar();
while(!isdigit(ch)) { if(ch == '-') f = -1; ch = getchar(); }
while(isdigit(ch)) { x = x * 10 + ch - 48; ch = getchar(); }
return f * x;
}
int qp(int n, int m = mod - 2) {
int res = 1;
while(m) {
if(m & 1) res = 1ll * res * n % mod;
m >>= 1, n = 1ll * n * n % mod;
}
return res;
}
using std::vector;
int n, m;
struct Tree : vector<vector<int> > {
void add(int u, int v)
{ this -> at(u).push_back(v), this -> at(v).push_back(u); }
void build(int n) {
(*this).resize(n);
for(int i = 1, x, y; i < n; ++i) {
x = read() - 1, y = read() - 1;
(*this).add(x, y);
}
}
};
void del(vector<int> &l, int x) {
for(auto it = l.begin(); it != l.end();)
((*it) == x) ? it = l.erase(it) : ++it;
}
Tree getTree(Tree t, int x) {
std::queue<int> q; q.push(x);
while(!q.empty()) {
int u = q.front(); q.pop();
for(int v : t[u])
q.push(v), del(t[v], u);
}
return t;
}
vector<int> f[N][M];
void init(void) {
for(int i = 0; i < n; ++i)
for(int j = 0; j < m; ++j) f[i][j].clear();
}
int dp(Tree &S, Tree &T, int s, int t) {
vector<int> &g = f[s][t];
if(!g.empty()) return g.back();
g.resize(1 << T[t].size()), g[0] = 1;
for(int v : S[s])
for(int i = g.size() - 1; ~i; --i)
for(int j = 0; j < T[t].size(); ++j) if((i >> j) & 1)
if(g[i ^ (1 << j)])
(g[i] += 1ll * g[i ^ (1 << j)] * dp(S, T, v, T[t][j]) % mod) %= mod;
return g.back();
}
int solvE(Tree &S, Tree &T) {
if(S.size() < T.size()) return 0;
if(T.size() <= 1) return S.size();
Tree s = getTree(S, 0); int res = 0;
for(int i = 0; i < T.size(); ++i) {
Tree t = getTree(T, i); init();
for(int j = 0; j < S.size(); ++j)
(res += dp(s, t, j, i)) %= mod;
}
return res;
}
Tree S, T;
signed main(void) {
S.build(n = read()), T.build(m = read());
int res = 1ll * solvE(S, T) * qp(solvE(T, T)) % mod;
return printf("%d\n", res), 0;
}