20240924 练习记录
3 个 DP,还想了几道题,但不会。
*P3349 [ZJOI2016] 小星星
考虑树上的点最终会对应在图上的哪个点,设 \(f_{x,i}\) 表示树上的点 \(x\) 对应图上点 \(i\) 时的方案数,当 \(x\) 对应 \(i\) 后,在树上 \(x\) 的所有子节点也必须像在树上一样,在图上和 \(i\) 之间有连边,有了这条限制,可以写出状态转移方程 \(f_{x,i} = \prod_{y\in\text{son}(x)}\sum_{j=1}^n f_{y,j}\times g_{i,j}\),其中 \(g\) 是图的邻接矩阵。
做到这里发现可能会有算重的部分,也就是说,树上有些点映射到图上的同一个点了,但是不要急着放弃,这个重复其实可以容斥掉!枚举所有点集合的子集 \(S\),然后强制只能映射到图上的这些点中。\(\text{popcount}(S)=n\) 时加答案,\(\text{popcount}(S)=n-1\) 的时候扣掉……以此类推。时间复杂度 \(\mathcal{O}(n^32^n)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
// #define int long long
const int N = 20;
int n,m;
int g[N][N],_g[N][N];
vector<int> G[N];
long long ans, f[N][N];
void dfs(int x,int fa){
for (int i = 1;i <= n;i++) f[x][i] = 1;
for (int y : G[x]){
if (y == fa) continue;
dfs(y,x);
for (int i = 1;i <= n;i++){
long long sum = 0;
for (int j = 1;j <= n;j++)
sum += 1ll * f[y][j] * g[i][j];
f[x][i] *= sum;
}
}
}
signed main(){
cin >> n >> m;
for (int i = 1;i <= m;i++){
int x,y; cin >> x >> y;
g[x][y] = g[y][x] = 1;
_g[x][y] = _g[y][x] = 1;
}
for (int i = 1;i < n;i++){
int x,y; cin >> x >> y;
G[x].push_back(y);
G[y].push_back(x);
}
for (int S = 1;S < (1 << n);S++){
memset(f,0,sizeof(f));
for (int i = 1;i <= n;i++){
if (!(S & (1 << (i - 1)))){
for (int j = 1;j <= n;j++)
g[i][j] = g[j][i] = 0;
}
}
dfs(1,0);
long long res = 0;
for (int i = 1;i <= n;i++){
if (S & (1 << (i - 1))){
res += f[1][i];
}
}
if (__builtin_popcount((1 << n) - S - 1) & 1)
ans -= res;
else ans += res;
memcpy(g,_g,sizeof(g));
}
cout << ans;
return 0;
}
P6419 [COCI2014-2015#1] Kamp
很恶心的换根 DP!注意到点 \(i\) 最终的答案是,\(i\) 到所有关键点路径并 \(\times2\) 然后扣去与所有关键点里最大的那个的距离。到这里其实就做完了,细节非常多!求子树外最远距离需要记录次大值这是经典套路,求路径并的时候,要讨论子树内子树外是否有关键点的情况,在代码中标注出来了。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e5 + 5;
int n,k,f[N],g[N],_g[N],h[N],_h[N],cnt[N]; bool v[N];
struct Edge { int y,z; };
vector<Edge> G[N];
void dfs(int x,int fa){
if (v[x]) cnt[x]++;
for (auto e : G[x]){
int y = e.y, z = e.z;
if (y == fa) continue;
dfs(y,x);
cnt[x] += cnt[y];
if (cnt[y]) // !
f[x] += z + f[y];
if (cnt[y]){
if (g[y] + z > g[x]){
h[x] = g[x]; _h[x] = _g[x];
g[x] = g[y] + z; _g[x] = y;
}
else if (g[y] + z == g[x]){
h[x] = g[y] + z; _h[x] = y;
}
else if (g[y] + z > h[x]){
h[x] = g[y] + z; _h[x] = y;
}
}
}
}
void dfs2(int x,int fa){
for (auto e : G[x]){
int y = e.y, z = e.z;
if (y == fa){
if (k - cnt[x] == 0) continue; // !
f[x] = f[y];
if (cnt[x] == 0) f[x] += z; // !
if (_g[y] == x){
if (h[y] + z > g[x])
g[x] = h[y] + z, _g[x] = y;
else if (h[y] + z == g[x])
h[x] = h[y] + z, _h[x] = y;
else if (h[y] + z > h[x])
h[x] = h[y] + z, _h[x] = y;
}
else{
if (g[y] + z > g[x])
g[x] = g[y] + z, _g[x] = y;
else if (g[y] + z == g[x])
h[x] = g[y] + z, _h[x] = y;
else if (g[y] + z > h[x])
h[x] = g[y] + z, _h[x] = y;
}
continue;
}
}
for (auto e : G[x]){
int y = e.y, z = e.z;
if (y == fa) continue;
dfs2(y,x);
}
}
signed main(){
cin >> n >> k;
for (int i = 1;i < n;i++){
int x,y,z; cin >> x >> y >> z;
G[x].push_back({y,z}), G[y].push_back({x,z});
}
for (int i = 1;i <= k;i++){
int x; cin >> x;
v[x] = 1;
}
dfs(1,0);
dfs2(1,0);
for (int i = 1;i <= n;i++)
cout << f[i] * 2 - g[i] << '\n';
return 0;
}
**[ARC108E] Random IS
思考的时候从思维固化了,一直从顺序 DP 的角度去思考,然后就爆了啊。如果固定住一个区间 \([l,r]\),那么区间外不管怎么选择,都不会影响到区间内的信息。那就可以考虑区间 DP。设 \(f_{l,r}\) 表示必须选 \(a_l,a_r\),区间 \([l,r]\) 内标记个数的期望,区间 \((l,r)\) 内有 \(cnt\) 个不错的。转移直接枚举中间点 \(k\),\(f_{l,r}=\dfrac{1}{k}\sum_{l<k<r,a_l < a_k < a_r}f_{l,k}+f_{k,r}+1\)。
这样时间复杂度是 \(O(n^3)\) 的,发现枚举过程中,\(l<k<r,a_l<a_k<a_r\) 是一个二维数点问题,于是可以固定下标,用权值树状数组求解,每次更新加入 \(f_{l,r}\) 即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 5, P = 1e9 + 7;
// #define int long long
int n,a[N];
struct SZSZ{
int c[N];
void add(int x,int y){
for (;x < N;x += x & -x) (c[x] += y) %= P;
}
int sum(int x){
int y = 0;
for (;x;x -= x & -x) (y += c[x]) %= P;
return y;
}
int query(int l,int r){
return (sum(r) - sum(l - 1) + P) % P;
}
}t[N][3];
int qpow(int a,int b){
int c = 1;
for (;b;b >>= 1,a = 1ll * a * a % P)
if (b & 1)
c = 1ll * c * a % P;
return c;
}
long long f[N][N];
signed main(){
cin >> n;
for (int i = 1;i <= n;i++)
cin >> a[i], a[i]++;
a[0] = 1, a[n + 1] = n + 2;
for (int l = n;l >= 0;l--){
for (int r = l + 1;r <= n + 1;r++){
if (a[l] > a[r]) continue;
int cnt = t[l][2].query(a[l] + 1,a[r] - 1);
if (cnt)
f[l][r] = 1ll * (t[l][0].query(a[l] + 1,a[r] - 1) + t[r][1].query(a[l] + 1,a[r] - 1)) * qpow(cnt,P - 2) % P + 1;
t[l][2].add(a[r],1), t[l][0].add(a[r],f[l][r]), t[r][1].add(a[l],f[l][r]);
}
}
cout << f[0][n + 1];
return 0;
}
*\(\color{red}{\text{[ARC184D] Erase Balls 2D}}\)
想了很久还是不会。
*\(\color{red}{\text{CF708E Student's Camp}}\)
想到了和一篇题解类似的做法,但只会用一次的前缀和优化,再往后晕了,先咕着明天写。