NOIP模拟33
T1:
考虑状压dp,发现传统转移需要记录当前猎人状态
数据范围显然不可行
考虑转化思路,发现问题所求期望可以进行转化,
利用期望线性性质对期望进行分解,具体表现为:
转化题意:1号猎人死亡轮数期望等价于其余猎人在1号前
死亡期望 + 1,由于其余猎人在1号前死亡贡献为1,故可
直接转化为概率,由于概率期望问题均匀性,故i号猎人比
1号猎人先死概率为W[i] / (W[i] + W[1]),累加即可
1 #include <bits/stdc++.h> 2 using namespace std; 3 #define I int 4 const I N = 1e5 + 7; 5 const I mod = 998244353; 6 I n,res,base; 7 inline I qpow (I a,I b) { I res (1); 8 for ( ; b ;a = 1ll * a * a % mod, b >>= 1) 9 if (b & 1) res = 1ll * res * a % mod; return res; 10 } 11 signed main () { 12 scanf ("%d%d",&n,&base); 13 for (I i(2),tmp;i <= n; ++ i) scanf ("%d",&tmp), 14 res = (res + 1ll * tmp * qpow ((base + tmp) % mod,mod - 2) % mod) % mod; 15 printf ("%lld\n", 1ll * ++ res); 16 }
期望求解:定义式:概率乘贡献,线性分解
T2:
考虑记录子节点状态转移到父节点,利用bitset维护,
发现空间复杂度过高,考虑使用内存池或分块进行维护,
发现事实上每个节点求解过程中都需要其所有子节点信息
故无法用上述方法进行空间维护
考虑转化思路,发现问题本质在于子节点信息的继承
考虑使用合并类思想,实际采用线段树合并或set启发式合
并,于是发现设L为前缀0数,R为后缀0数,MID为中缀0数
则问题实际上是求max (MID,R + L) 线段树维护即可
1 #include <bits/stdc++.h> 2 using namespace std; 3 #define I int 4 #define V void 5 const I N = 1e5 + 7; 6 I n,m,q,u,v,p,tot,head[N],to[N],nxt[N],root[N],out[N]; 7 vector <I> s[N]; 8 inline V found (I x,I y) { 9 to[++tot] = y, nxt[tot] = head[x], head[x] = tot; 10 } 11 struct SGT { 12 #define mid (l + r >> 1) 13 I sgt,pre[N*80],nxt[N*80],middle[N*80],lc[N*80],rc[N*80]; 14 inline V update (I x,I l,I r) { 15 if (!lc[x] && !rc[x]) { 16 pre[x] = nxt[x] = middle[x] = r - l + 1; 17 } else 18 if ( lc[x] && rc[x]) { 19 pre[x] = pre[lc[x]], nxt[x] = nxt[rc[x]]; 20 middle[x] = max (middle[lc[x]],middle[rc[x]]); 21 middle[x] = max (middle[x],pre[rc[x]] + nxt[lc[x]]); 22 } else 23 if ( lc[x] && !rc[x]) { 24 pre[x] = pre[lc[x]], nxt[x] = nxt[lc[x]] + r - mid; 25 middle[x] = max (middle[lc[x]],nxt[x]); 26 } else 27 if ( rc[x] && !lc[x]) { 28 pre[x] = pre[rc[x]] + mid - l + 1, nxt[x] = nxt[rc[x]]; 29 middle[x] = max (middle[rc[x]],pre[x]); 30 } 31 } 32 V insert (I &x,I l,I r,I pos) { 33 if (!x) x = ++ sgt; if (l == r) return ; 34 pos <= mid ? insert (lc[x],l,mid,pos) : insert (rc[x],mid + 1,r,pos); 35 update (x,l,r); 36 } 37 I merge (I x,I y,I l,I r) { 38 if (!x || !y) return x + y; if (l == r) return x; 39 lc[x] = merge (lc[x],lc[y],l,mid); 40 rc[x] = merge (rc[x],rc[y],mid + 1,r); 41 update (x,l,r); 42 return x; 43 } 44 }SGT; 45 V dfs (I x) { 46 for (I i(head[x]),y(to[i]); i ;i = nxt[i],y = to[i]) 47 dfs (y), root[x] = SGT.merge (root[x],root[y],1,m); 48 for (auto y : s[x]) SGT.insert (root[x],1,m,y); 49 if (!root[x]) return (V) (out[x] = -1); 50 out[x] = max (SGT.middle[root[x]],SGT.pre[root[x]] + SGT.nxt[root[x]]); 51 } 52 signed main () { 53 cin >> n >> m >> q; 54 for (I i(1);i < n; ++ i) cin >> u >> v, found (u,v); 55 while (q -- ) cin >> u >> p, s[u].push_back (p); dfs (1); 56 for (I i(1);i <= n; ++ i) cout << out[i] << endl; 57 }
信息继承的合并思路,问题不仅要微观思考细节,还要宏观
把控其本质。动态开点线段树与线段树合并的思想
T3:
数据范围提示状压,考虑问题本质状态为有且仅有一条
链连接1-n,于是考虑记录点集状态,考虑新加入点的影响
first:加入点位于当前链末端,second:加入点位于某
点集,于是需要记录链末元素,分别进行转移即可
预处理点集信息降低复杂度
1 #include <bits/stdc++.h> 2 using namespace std; 3 #define I int 4 const I N = 15; 5 I n,m,u,v,w,dis[N + 1][N + 1],dp[1 << N][N + 1],sum1[1 << N],sum2[1 << N][N + 1]; 6 signed main () { 7 cin >> n >> m; 8 while (m -- ) cin >> u >> v >> w, dis[u][v] = dis[v][u] = w; 9 for (I i(1);i < 1 << n; ++ i) for (I j(1);j <= n; ++ j) 10 for (I k(j + 1);k <= n; ++ k) if (i & 1 << j - 1 && i & 1 << k - 1) 11 sum1[i] += dis[j][k]; 12 for (I i(1);i < 1 << n; ++ i) for (I j(1);j <= n; ++ j) if (!(i & 1 << j - 1)) 13 for (I k(1);k <= n; ++ k) if (i & 1 << k - 1 && dis[j][k]) 14 sum2[i][j] += dis[j][k]; 15 memset (dp,-1,sizeof dp); dp[1][1] = 0; 16 for (I i(1);i < 1 << n; ++ i) 17 for (I j(1);j <= n; ++ j) if (dp[i][j] != -1) { 18 for (I k(1);k <= n; ++ k) if (!(i & 1 << k - 1) && dis[j][k]) 19 dp[i | 1 << k - 1][k] = max (dp[i | 1 << k - 1][k],dp[i][j] + dis[j][k]); 20 I state (i ^ (1 << n) - 1); 21 for (I k(state); k ; -- k &= state) 22 dp[i | k][j] = max (dp[i | k][j],dp[i][j] + sum1[k] + sum2[k][j]); 23 } 24 printf ("%d\n",sum1[(1 << n) - 1] - dp[(1 << n) - 1][n]); 25 }
dp过程的方向性使得转移过程中省去冗余判断,另一种解释
为为了保证dp转移的方向性,在转移过程中不能出现后向转
移实际降低了问题复杂度。考虑dp设计原理,预处理降低复
杂度但要注意分析预处理的状态的需求量,实际分析。
拓展:子集枚举
1 设全集为 N = (1 << n) - 1; 2 tps1:全集枚举 3 正序:for (int i = 1;i <= N; ++ i) 4 倒序:for (int i = N; i ; -- i) 5 设目标集为 N = s; 6 tps2:倒序枚举 7 for (int i = N; i ;i = i - 1 & N) 8 原理:状态合并 9 事实上存在冗余枚举,为保证枚举N的子集 10 在枚举过程中通过 & N保证状态合法性 11 tps3:正序枚举状态数为k子集 12 int M = (1 << m) - 1; 13 int tmp1, tmp2; 14 for (int i = M;i <= N;i = (i & ~tmp2) / tmp1 >> 1 | tmp2) 15 tmp1 = lowbit(i), tmp2 = i + tmp1; 16 原理:模拟进位(手模即可),详见如下: 17 first : tmp2 = i + tmp1 --> 将最后一位左移(注意存在进位情况) 18 second: i & ~tmp2 --> 左移的变化位置(预处理进位的情况) 19 third : second / tmp1 >> 1 --> 将变化位置移动到末尾(保证状态数) 20 forth : third | tmp2 --> 生成正序下状态数为k子集