传送门1或者传送门2
注意,空间限制是8MB!
这题太综合了,能独立写出来说明你能在澳门站拿金了。另外,树链剖分优化空间复杂度真的是我第一次听说~
首先,它考察了一个博弈论的结论:若干石堆的Nim游戏,后手必胜等价于各石堆数异或和为0。所以原题等价于以下问题:保留一个子集(可空)的石堆,a属性的异或和为0,且b属性的和最大。这显然是一个01背包(大概每一个关注“树链剖分”的人都会吧~),dp表示保留出的最大b属性和,bTot - dp[idx][0]即所求。于是我们有如下代码:
#includeusing namespace std; typedef long long LL; typedef unsigned long long ULL; #define rep(i,a,b) for(int i = (a);i <= (b);++i) #define re_(i,a,b) for(int i = (a);i < (b);++i) #define dwn(i,a,b) for(int i = (a);i >= (b);--i) const int N = 20000 + 5; int n,m,totVal,a[N],b[N];vector > dp; void dbg(){puts("");} template void dbg(const T &f, const R &... r) { cout << f << " "; dbg(r...); } template inline void read(Type &xx){ Type f = 1;char ch;xx = 0; for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1; for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0'; xx *= f; } int calc(int i,int j){ if(!i) return !j ? 0 : INT_MIN; if(dp[i].count(j)) return dp[i][j]; int v1 = dp[i-1].count(j^a[i]) ? dp[i-1][j^a[i]] : calc(i-1,j^a[i]), v2 = dp[i-1].count(j) ? dp[i-1][j] : calc(i-1,j); return dp[i][j] = max(v1 > INT_MIN ? v1 + b[i] : INT_MIN,v2); } int main(int argc, char** argv) { read(m);n = 0;totVal = 0; dp.push_back(unordered_map ());dp[0][0] = 0; while(m--){ char op[5];scanf("%s",op); if(op[0] == 'A'){ ++n;read(a[n]);read(b[n]); totVal += b[n]; dp.push_back(unordered_map ()); calc(n,0); } else{ dp.pop_back(); totVal -= b[n]; --n; } printf("%dn",totVal - dp[n][0]); } return 0; }
可惜它卡不过去,test case2就MLE了……后来才了解到,可以用离线+轻重链剖分来优化空间。
待填坑:OJ中空间使用的计算方式。
首先,我们使用离线算法,把询问组织成树,即:
- 维护一个u表示当前指向的点。
- ADD *** 作,新建一个点v,并且u是v的父亲。
- DEL *** 作,u = fa[u]。
- 维护tim,tim[u]表示点u对应的查询编号的集合。
然后我们考虑这棵树的每个点,不妨设当前在考虑u。我们要求的是:从根到u的路径所表示的石堆的01背包。于是我们需要cur变量表示fa[u]的dp数组编号,即dp[cur]表示fa[u]的dp数组。此时我们有两个选择,把u的dp数组保存在dp[cur]或dp[cur+1]。假如没有轻重链剖分的想法,那么cur会达到这棵树的最大深度,于是这个算法和上面的算法没有任何区别。
因此现在我们希望能够复用dp[cur]的空间。我们痛苦地发现,ufa有若干儿子,如果某个儿子u0复用了dp[cur]的空间,那么信息丢失了,后续的儿子都无法求出答案。所以复用dp[cur]空间的儿子只能有一个,并且这个儿子必须最后遍历,不妨把它叫做“红点”。此时cur可能达到的最大值为:根到叶子的路径的非红点最大数目。
什么时候非红点最少呢?采用轻重链剖分的时候(更新:不一定是最优,但至少是一个比较优秀的解答)。因此红点就是重儿子。下面简要说明为什么路径上轻儿子数目不超过logn:轻儿子v满足:2*siz[v] <= siz[fa[v]] <= n,因此siz至多取logn次轻儿子就到了叶子。实际上,这就是树链剖分一条基本性质:轻儿子数目,就是重链的数目,不超过logn。
因此我们的 *** 作如下:如果当前点u是轻儿子,则不复用dp[cur]空间,新开空间来存储。否则覆盖dp[cur]。之后先遍历轻儿子,再遍历重儿子。
代码#includeusing namespace std; typedef long long LL; typedef unsigned long long ULL; #define rep(i,a,b) for(int i = (a);i <= (b);++i) #define re_(i,a,b) for(int i = (a);i < (b);++i) #define dwn(i,a,b) for(int i = (a);i >= (b);--i) const int N = 20000 + 5,M = 16384; int n,m,a[N],b[N],btot[N],siz[N],son[N]; vector tim[N],G[N]; int ans[N<<1],cur = 0,dp[23][M+5],tmp[M+5]; void dbg(){puts("");} template void dbg(const T &f, const R &... r) { cout << f << " "; dbg(r...); } template inline void read(Type &xx){ Type f = 1;char ch;xx = 0; for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1; for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0'; xx *= f; } void dfs1(int u,int ufa = 0){ siz[u] = 1;btot[u] = b[u] + btot[ufa];int mx = 0; for(int v: G[u]){ dfs1(v,u); siz[u] += siz[v]; if(mx < siz[v]){ mx = siz[v]; son[u] = v; } } } void dfs2(int u,bool use = true){ memcpy(tmp,dp[cur],sizeof tmp); re_(i,0,M) if(~dp[cur][i]){ tmp[i^a[u]] = max(dp[cur][i^a[u]],dp[cur][i]+b[u]); } for(int t: tim[u]) ans[t] = btot[u]-tmp[0]; if(use) --cur; memcpy(dp[++cur],tmp,sizeof tmp); for(int v: G[u]){ if(v == son[u]) continue; dfs2(v,false);//轻儿子不复用。1号点也是轻儿子,但可以复用 } if(son[u]) dfs2(son[u],true); if(!use) --cur; } int main(int argc, char** argv) { read(m);n = 1;int u = 1; rep(cas,1,m){ char op[5];scanf("%s",op); if(op[0] == 'A'){ ++n;read(a[n]);read(b[n]); G[u].push_back(n);tmp[n] = u; u = n; } else{ u = tmp[u]; } tim[u].push_back(cas); } dfs1(1); //-1表示负无穷 memset(dp[0],-1,sizeof dp[0]);dp[0][0] = 0; dfs2(1); rep(i,1,m) printf("%dn",ans[i]); return 0; }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)