BZOJ2310 ParkII 【插头DP】

题目描述:

给一个n*m的矩阵,n<=100, m<=8
每个点有权值,一条路径经过每个点最多一次,求路径权值的最大值
(路径可能只有一个点)

题目分析:

妥妥的插头DP了,不过这道题是求路径而不是环,需要一种叫做独立插头的东西。第一次打看别人动不动3000B+的代码真的是心态爆炸…

如果用括号表示法的话就新增一个3号插头,加上对应的操作:

  • 当前没有插头,可以直接转移,可以新建一个(1,2),或者(3)向下,或者(3)向右
  • 当前有一个插头,延伸即可
  • 当前有两个插头,如果是(1,2)的话不能转移(成环了);(1,3)或者(2,3)的话就把左(右)括号对应的另一个插头改成独立插头;(3,3)可以统计答案,但不能转移
    除了当前方块其它地方都是0的时候可以统计答案

但是会加上巨多if,代码量直线飙升。。

所以我们采用最小表示法,每段轮廓线维护连通性(即属于哪个连通分量),由于独立插头最多有两个,且m<=8,所以连通分量个数<=5
我们采用8进制储存(相对于括号表示法的4进制会慢一些,但是最小表示法的编程复杂度更低)
同时encode的方式变更一下:

inline void decode(int x){
	dt=x&3,x>>=2;
	for(int i=m;i>=0;i--) code[i]=x&7,x>>=3;
}
inline int encode(int m){
	int res=0,cnt=0;
	memset(ch,-1,sizeof ch);ch[0]=0;
	for(int i=0;i<=m;i++){//最小表示法重标号
		if(ch[code[i]]==-1) ch[code[i]]=++cnt;
		code[i]=ch[code[i]];
		res=(res<<3)|code[i];//8进制储存
	}
	res=(res<<2)|dt;//dt表示独立插头的个数,下面会讲到
	return res;
}

新建连通分量的时候赋值为13(极大值),方便重标号。

如果按照原来的j==m时就shift()的方法在情况较多时容易乱,我们发现,shift操作就相当于把现在的0 ~ m-1位移到1 ~ m位,那么把0看做高位,就相当于我们只用encode现在的0~m-1位,反正移了之后首位是0。
也就是说, encode时,j==m时带入m-1, j!=m时带入m, 就可以替代shift操作了。

dt表示独立插头的个数,因为合法状态的独立插头个数<=2,借此可以剔除无效状态,将dt作为最后两位压入状态中

接着看一看主要操作:

inline void dp(int i,int j)
{
	for(int k=1;k<=h[cur].tot;k++)
	{
		decode(h[cur].val[k]);
		int &L=code[j-1],&U=code[j],num=h[cur].f[k]+mp[i][j];
		if(!L&&!U){
			h[!cur].insert(encode(j==m?m-1:m),num-mp[i][j]);
			if(i<n&&j<m) L=U=13,h[!cur].insert(encode(m),num);
			if(dt++<2){
				if(i<n) L=13,U=0,h[!cur].insert(encode(j==m?m-1:m),num);
				if(j<m) L=0,U=13,h[!cur].insert(encode(m),num);
			}
		}
		else if(!L||!U){
			L=U+L,U=0; if(i<n) h[!cur].insert(encode(j==m?m-1:m),num);
			U=L,L=0; if(j<m) h[!cur].insert(encode(m),num);
			L=U=0; if(dt++<2) h[!cur].insert(encode(j==m?m-1:m),num);
		}
		else if(L&&U){
			if(L==U) continue;
			for(int t=0,c=U;t<=m;t++) if(code[t]==c) code[t]=L;
			L=U=0,h[!cur].insert(encode(j==m?m-1:m),num);
		}
	}
}

转移看起来合情合理也并不难打
但是我们的答案呢?!ans到哪里统计?!
仔细研究一下上面的代码:

在只有一个插头时,我们有一个L=U=0表示终止这个插头的操作。
如果最初不是一个独立插头,意味着它现在变成了独立插头,用于应对例如下面的情况:
在这里插入图片描述
如果最初是一个独立插头,意味着形成了一条路径,本来是应该统计答案的,但是我们可以把它保留下来,同时让它的独立插头数量保持在2个,这样到最后的时候这个状态转移出的合法状态一定只会包含这条路径,不会再有其它的东西了。

上面所说同样适用于有两个插头的情况:
两个插头如果都是独立插头,本该统计答案然后结束转移,但是可以保留到最后。
如果不都是独立插头,那么相当于合并连通分量,不会有什么影响,对应的答案可以被两个都是独立插头的情况统计到。

综上所述,我们的代码可以变得十分简洁,中间过程并不用统计答案,只需要统计最后状态即可(一个点的情况在输入时取max就行了):

for(int i=1;i<=h[cur].tot;i++) ans=max(ans,h[cur].f[i]);

附上完整代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 15;
const int mod = 40007;
const int N = 1e6+5;
struct HashMap{
	int val[N],fir[mod],nxt[N],tot;
	int f[N];
	inline void init(){memset(fir,0,sizeof fir);tot=0;}
	inline void insert(int x,int w)
	{
		int u=x%mod;
		for(int i=fir[u];i;i=nxt[i]) if(val[i]==x) {f[i]=max(f[i],w);return;}
		nxt[++tot]=fir[u],fir[u]=tot,val[tot]=x,f[tot]=w;
	}
}h[2];
int n,m,cur,code[maxn],dt,ch[maxn];
int mp[105][maxn],ans=-0x3f3f3f3f;
inline void decode(int x){
	dt=x&3,x>>=2;
	for(int i=m;i>=0;i--) code[i]=x&7,x>>=3;
}
inline int encode(int m){
	int res=0,cnt=0;
	memset(ch,-1,sizeof ch);ch[0]=0;
	for(int i=0;i<=m;i++){
		if(ch[code[i]]==-1) ch[code[i]]=++cnt;
		code[i]=ch[code[i]];
		res=(res<<3)|code[i];
	}
	res=(res<<2)|dt;
	return res;
}
inline void dp(int i,int j)
{
	for(int k=1;k<=h[cur].tot;k++)
	{
		decode(h[cur].val[k]);
		int &L=code[j-1],&U=code[j],num=h[cur].f[k]+mp[i][j];
		if(!L&&!U){
			h[!cur].insert(encode(j==m?m-1:m),num-mp[i][j]);
			if(i<n&&j<m) L=U=13,h[!cur].insert(encode(m),num);
			if(dt++<2){
				if(i<n) L=13,U=0,h[!cur].insert(encode(j==m?m-1:m),num);
				if(j<m) L=0,U=13,h[!cur].insert(encode(m),num);
			}
		}
		else if(!L||!U){
			L=U+L,U=0; if(i<n) h[!cur].insert(encode(j==m?m-1:m),num);
			U=L,L=0; if(j<m) h[!cur].insert(encode(m),num);
			L=U=0; if(dt++<2) h[!cur].insert(encode(j==m?m-1:m),num);
		}
		else if(L&&U){
			if(L==U) continue;
			for(int t=0,c=U;t<=m;t++) if(code[t]==c) code[t]=L;
			L=U=0,h[!cur].insert(encode(j==m?m-1:m),num);
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%d",&mp[i][j]),ans=max(ans,mp[i][j]);
	h[cur].insert(0,0);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
		{
			h[!cur].init();
			dp(i,j);
			cur=!cur;
		}
	for(int i=1;i<=h[cur].tot;i++) ans=max(ans,h[cur].f[i]);
	printf("%d",ans);
}

相较之下,最小表示法的时间大概是括号表示法的2~3倍,但是代码量只有后者的一半,并且维护连通性的做法更具有普适性。当然,对于简单回路问题来说,则括号表示法优势更明显。

仔细思考的读者可能会发现,实际上需要统计答案的状态一定是轮廓线上都为0且dt==2的状态,且这样的状态不需要再转移了,所以可以适用这样一个函数让频繁出现的插入操作更简洁:

inline void ins(int j,int num){
	int x=encode(j==m?m-1:m);
	if(!(x>>2)&&dt==2) ans=max(ans,num);
	else h[!cur].insert(x,num);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值