ICPC澳门20I——树链剖分优化空间复杂度,好题

ICPC澳门20I——树链剖分优化空间复杂度,好题,第1张

ICPC澳门20I——树链剖分优化空间复杂度,好题

传送门1或者传送门2

注意,空间限制是8MB!

这题太综合了,能独立写出来说明你能在澳门站拿金了。另外,树链剖分优化空间复杂度真的是我第一次听说~

首先,它考察了一个博弈论的结论:若干石堆的Nim游戏,后手必胜等价于各石堆数异或和为0。所以原题等价于以下问题:保留一个子集(可空)的石堆,a属性的异或和为0,且b属性的和最大。这显然是一个01背包(大概每一个关注“树链剖分”的人都会吧~),dp表示保留出的最大b属性和,bTot - dp[idx][0]即所求。于是我们有如下代码:

#include 
using 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("");}
templatevoid dbg(const T &f, const R &... r) {
    cout << f << " ";
    dbg(r...);
}
templateinline 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]。之后先遍历轻儿子,再遍历重儿子。

代码
#include 
using 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("");}
templatevoid dbg(const T &f, const R &... r) {
    cout << f << " ";
    dbg(r...);
}
templateinline 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;
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5155255.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-18
下一篇 2022-11-18

发表评论

登录后才能评论

评论列表(0条)

保存