zoukankan      html  css  js  c++  java
  • P5391

    自己想出来一个奇怪的(复杂度是严格的)序列分块(遇到困难分大块!)。看了题解发现是巧妙的链分治(想不到啊想不到)。两种方法都写一下吧。

    首先这是个显然的带撤销完全背包。但是带撤销要求记录所有历史版本,这样空间也是平方的,就爆炸了。于是考虑优化空间同时保证时间是平方(第一次见到用空间限制加强题目难度的)。

    正解:重链剖分

    这题允许离线,我们将加入和撤销操作离线下来可以得到一个与之对应的操作树(我曾经点开过一个建撤销树 + LCT 的 CF 题,然而不会 LCT 就关闭了页面,只记得这个建树的套路),那么某个节点处的答案就是根节点到它的路径上物品的完全背包。

    考虑一遍 dfs,维护递归栈。那么这和朴素做法根本没有任何区别,因为递归栈是 (mathrm O(max dep)) 的,一条链就到 (mathrm O(n))​ 了。。。。不过我们有这样一个优化的思路:考虑当前在点 (x),那么在它访问最后一个访问的儿子树时,可以不在递归栈中保留 (x) 处的背包,直接修改成最后一个儿子处的背包,因为最后一个儿子后面没有撤销操作了(这体现了离线的优点:可以知道当前是不是最后一个撤销)。这样看似只能省一点点空间,但是我们可以改变儿子的访问顺序,钦定某个儿子最后一个访问。「钦定一个特殊的儿子」是不是想到了链分治?考虑重剖,那么每条链上经过的不特殊的儿子数量是 log,也就是说递归栈中时刻只有 log 个元素,空间就是 (mathrm O(mlog n)) 了。或者你长剖可以做到 (mathrm O(msqrt n))​​ 也可以。

    结构体写背包比较爽。

    code
    #include<bits/stdc++.h>
    using namespace std;
    #define pb push_back
    const int N=2e4+10;
    int m,qu,n;
    int v[N],w[N];
    int fa[N];
    vector<int> nei[N];
    vector<int> qry[N];
    int wson[N],sz[N];
    void dfs1(int x=n+1){
    	sz[x]=1;
    	for(int i=0;i<nei[x].size();i++){
    		int y=nei[x][i];
    		dfs1(y);
    		sz[x]+=sz[y];
    		if(sz[y]>sz[wson[x]])wson[x]=y;
    	}
    }
    struct knap{
    	int dp[N];
    	knap(){memset(dp,0,sizeof(dp));}
    	void add(int V,int W){
    		for(int i=W;i<=m;i++)dp[i]=max(dp[i],dp[i-W]+V);
    	}
    };
    vector<knap> stk;
    int ans[N];
    void dfs(int x=n+1){
    	for(int i=0;i<qry[x].size();i++)ans[qry[x][i]]=stk.back().dp[m];
    	for(int i=0;i<nei[x].size();i++){
    		int y=nei[x][i];
    		if(y==wson[x])continue;
    		stk.pb(stk.back());stk.back().add(v[y],w[y]);
    		dfs(y);
    		stk.pop_back();
    	}
    	if(wson[x])stk.back().add(v[wson[x]],w[wson[x]]),dfs(wson[x]);
    }
    int main(){
    	cin>>qu>>m;
    	int now=0;
    	for(int i=1;i<=qu;i++){
    		char op[10];
    		scanf("%s",op);
    		if(op[0]=='a')n++,scanf("%d%d",w+n,v+n),fa[n]=now,nei[now].pb(n),now=n,qry[n].pb(i);
    		else now=fa[now],qry[now].pb(i);
    	}
    	for(int i=1;i<=n;i++)if(fa[i]==0)fa[i]=n+1,nei[n+1].pb(i);
    	qry[n+1]=qry[0];
    	dfs1();
    	stk.pb(knap());dfs();
    	for(int i=1;i<=qu;i++)printf("%d
    ",ans[i]);
    	return 0;
    }
    

    歪解:分块

    考虑对任意时刻物品序列所在的模子(后面可能有一大截空的位置)这个静态的序列进行分块,块大小为 (B=sqrt n)。一个容易想到的思路是:任意时刻维护当前 (n) 所在块的所有位置的背包,以及之前所有块的第一个位置的背包。这样如果在块内移动那显然不需要担心;跨块的话,如果是添加,那么就将上一个块的背包全部删掉只留开头;如果是撤销,那就对撤销后所在块进行根号重构。

    但这样复杂度是假的,因为数据有可能在两相邻块的分界点反复横跳,时间复杂度就爆炸了。考虑针对性地补救一下这个情况:对于反复横跳,我们在第一次往右跳的时候并不删除上一块的背包们,这样除了第一次左跳,都不需要重构了。那么这样我们必定维护当前块所有和之前块首位,以及任意时刻往右跨块移动的时候并不立刻删除上一整块背包们,直到到下下块再删,也就是时刻最多只维护当前块和上一块这两块的整块。这样空间是 (2mB+mdfrac nB)​,依然能接受。

    但这只是针对特殊卡法的补救,能否适应一般情况呢?尝试构造卡的方法,发现这个算法表现得很丝滑,根本卡不掉。然后发现可以胡出来一个证明:

    1. 每次清空倒数第三块时,说明此时正在往右移过相邻块分界点,此时保留了上一整块的背包。如果想要再做一次清空操作,至少要往后走 (B) 步;如果想要做一次暴力重构,那么往前退一格是没有用的,必须往前退 (B) 步到达倒数第三块才会进行暴力重构。
    2. 每次暴力重构时,说明此时正在往左跨分界点,此时保留了该块一整块的背包。下一次清空,往右走一步按照算法并不会清空,只能往右走 (B) 步;下一次重构,肯定要往前退 (B) 步。

    也就是说「清空」和暴力重构这两种 (mathrm O(Bm)) 的操作都是必须隔 (B) 次才会有一次,总复杂度就是 (mathrm O!left(dfrac nBBm ight)=mathrm O(nm))​。

    btw,这题由于空间是线根,本身就比较卡,如果用 vector<knap> 实现会开两倍空间,就会 MLE 了。于是需要手写双向链表(u1s1 链表确实是除了 BIT 和 ufset 以外最好写的 ds),稳过好吧。

    code
    #include<bits/stdc++.h>
    using namespace std;
    #define pb push_back
    const int N=2e4+10;
    int qu,m;
    struct knap{
    	int dp[N];
    	knap(){memset(dp,0,sizeof(dp));}
    	void add(int V,int W){
    		for(int i=W;i<=m;i++)dp[i]=max(dp[i],dp[i-W]+V);
    	}
    };
    const int B=141;
    struct addedge{
    	int sz,siz[N],head[N],tail[N],prv[N],nxt[N];knap val[3*B+10];
    	addedge(){sz=0;}
    	vector<int> stk;
    	int nwnd(){
    		int p;
    		if(stk.size())p=stk.back(),stk.pop_back();
    		else p=++sz;
    		return p;
    	}
    	void pb(int x,knap y){
    		siz[x]++;
    		int p=nwnd();
    		val[p]=y;
    		prv[p]=tail[x],nxt[tail[x]]=p,tail[x]=p;
    	}
    	knap &back(int x){return val[tail[x]];}
    	void ppb(int x){
    		siz[x]--;
    		stk.pb(tail[x]);
    		int pv=prv[tail[x]];
    		nxt[pv]=0,prv[tail[x]]=0;
    		tail[x]=pv;
    	}
    }blk;
    int V[N],W[N];
    int main(){
    	cin>>qu>>m;
    	int n=0;
    	while(qu--){
    		char op[10];
    		cin>>op;
    		if(op[0]=='a'){
    			int w,v;
    			cin>>w>>v;
    			n++;
    			V[n]=v,W[n]=w;
    			if((n+B-1)/B!=(n+B-2)/B){
    				blk.pb((n+B-1)/B,n==1?knap():blk.back((n+B-2)/B));
    				blk.back((n+B-1)/B).add(v,w);
    				if((n+B-1)/B>=3)while(blk.siz[(n+B-1)/B-2]>1)blk.ppb((n+B-1)/B-2);
    			}
    			else{
    				blk.pb((n+B-1)/B,blk.back((n+B-1)/B));
    				blk.back((n+B-1)/B).add(v,w);
    			}
    		}
    		else{
    			if((n+B-1)/B!=(n+B-2)/B){
    				blk.ppb((n+B-1)/B);
    				if(blk.siz[(n+B-2)/B]==1){
    					int b=(n+B-2)/B,l=(b-1)*B+1,r=b*B;
    					for(int i=l+1;i<=r;i++)blk.pb(b,blk.back(b)),blk.back(b).add(V[i],W[i]);
    				}
    			}
    			else blk.ppb((n+B-1)/B);
    			n--;
    		}
    		printf("%d
    ",n?blk.back((n+B-1)/B).dp[m]:0);
    	}
    	return 0;
    }
    
    珍爱生命,远离抄袭!
  • 相关阅读:
    JDK动态代理源码分析
    使用docker-compose快速搭建本地ElasticSearch7和Elastichd环境
    IDEA导入SVN项目提示HTTPS:Server SSL certificate verification failed
    双重校验锁为什么要用volatile修饰
    Ribbon的基础知识
    Eureka的基础知识
    JDK1.8 JVM内存模型个人理解
    OAuth2+Zuul报RedisConnection.set([B[B)V解决方案
    Spring aop @aspect不生效问题
    教你使用markdown画程序流程图
  • 原文地址:https://www.cnblogs.com/ycx-akioi/p/solution-p5391.html
Copyright © 2011-2022 走看看