dp 练习记录...

streduc
(自己做出)
因为字符串拼接,从左到右考虑不太行。
把所有\(|S|\)集合都插入trie内
考虑把字符串消除过的点的连续段拿出来。
\(f_{l,r,p}\)表示原串\([l,r]\)区间,外部恰好有一个串,走到trie的\(p\)节点,是否可行。\(g_{l,r}\)表示\([l,r]\)是否能够消完。
转移:\(f_{l,r+1,nxt}|=f_{l,r,p}\),nxt表示\(x\)\(s_{r+1}\)后的节点。
如果\([r+1,t]\)能够被全部消完(\(g_{r+1,t}=1\)),则\(f_{l,t,p}|=f_{l,r,p}\)
如果存在一个\(p\),使得\(p\)是终止节点且\(f_{l,r,p}=1\)\(g_{l,r}=1\)
求出答案可以另外用一个dp。
\(h_i\)表示消除\([1,i]\)需要保留多少个字符。转移有\(h_i=\min(h_i,h_{i-1}+1)\)
如果\(g_{j,i}=1\)\(h_i=\min(h_i,h_{j-1})\)
发现数据范围太大,所以要把\(f_{l,r}\)视为一个bitset。

#include<bits/stdc++.h>
using namespace std;
#define N 260
char c[N],cc[N];
int n,ch[N*10][26],ct,bz[N*10],h[N],g[N][N],le;
bitset<N*3>f[N][N];
void ins(char *s){
	int x=0;
	for(int i=1;s[i];i++){
		if(!ch[x][s[i]-'a']){
			ch[x][s[i]-'a']=++ct;
		}
		x=ch[x][s[i]-'a'];
	}
	bz[x]=1;
}
int main(){
	scanf("%s%d",c+1,&n);
	le=strlen(c+1);
	for(int i=1;i<=n;i++){
		scanf("%s",cc+1);
		ins(cc);
	}
	memset(h,127,sizeof(h));
	for(int i=1;i<=le;i++)
		f[i][i-1][0]=1;
	for(int l=0;l<=le;l++)
		for(int i=1;i<=le;i++)
			if(i+l-1<=le){
				int r=i+l-1;
				for(int j=i;j<r;j++)
					if(g[j+1][r])
						f[i][r]|=f[i][j];
				for(int k=0;k<=ct;k++)
					if(f[i][r][k]&&r+1<=le&&ch[k][c[r+1]-'a'])
						f[i][r+1][ch[k][c[r+1]-'a']]=1;
				for(int k=0;k<=ct;k++)
					if(bz[k]&&f[i][r][k])
						g[i][r]=1;
			}
	h[0]=0;
	for(int i=1;i<=le;i++){
		h[i]=h[i-1]+1;
		for(int j=1;j<=i;j++)
			if(g[j][i])
				h[i]=min(h[i],h[j-1]);
	}
	printf("%d",h[le]);
}

Bookshelf
(自己做出)
简单题
考虑dp,设\(f_i\)表示摆放前\(i\)个,\(s\)是前缀和数组。
则有转移方程:\(f_i=\min(f_{j-1}+\max(h_j...h_i)),s_i-s_j\leq l\)
满足条件的\(j\)是一段连续区间可以二分。
移动\(i\)后,用线段树维护\(j\)下标处\(f_{j-1}+\max(h_j...h_i)\)的最大值。
显然建立单调栈后区间加法维护。

#include<bits/stdc++.h>
using namespace std;
#define N 1000010
#define int long long
int n,l,h[N],w[N],tg[N],mx[N],s[N],f[N],st[N],tp;
void pd(int o){
	if(tg[o]){
		tg[o*2]+=tg[o];
		tg[o*2+1]+=tg[o];
		mx[o*2]+=tg[o];
		mx[o*2+1]+=tg[o];
		tg[o]=0;
	}
}
void mod(int o,int l,int r,int x,int y){
	if(l==r){
		mx[o]=y;
		return;
	}
	int md=(l+r)/2;
	pd(o);
	if(x<=md)
		mod(o*2,l,md,x,y);
	else
		mod(o*2+1,md+1,r,x,y);
	mx[o]=min(mx[o*2],mx[o*2+1]);
}
void ad(int o,int l,int r,int x,int y,int z){
	if(r<x||y<l)
		return;
	if(x<=l&&r<=y){
		tg[o]+=z;
		mx[o]+=z;
		return;
	}
	pd(o);
	int md=(l+r)/2;
	ad(o*2,l,md,x,y,z);
	ad(o*2+1,md+1,r,x,y,z);
	mx[o]=min(mx[o*2],mx[o*2+1]);
}
int qu(int o,int l,int r,int x,int y){
	if(r<x||y<l)
		return 1e18;
	if(x<=l&&r<=y)
		return mx[o];
	int md=(l+r)/2;
	pd(o);
	return min(qu(o*2,l,md,x,y),qu(o*2+1,md+1,r,x,y));
}
int gt(int x,int po){
	int l=0,r=po,ans=0;
	while(l<=r){
		int md=(l+r)/2;
		if(s[md]>=x){
			ans=md;
			r=md-1;
		}
		else
			l=md+1;
	}
	return ans;
}
signed main(){
	scanf("%lld%lld",&n,&l);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&h[i],&w[i]);
		s[i]=s[i-1]+w[i];
	}
	memset(mx,32,sizeof(mx));
	mod(1,1,n,1,h[1]);
	for(int i=1;i<=n;i++){
		while(tp&&h[i]>=h[st[tp]]){
			ad(1,1,n,st[tp-1]+1,st[tp],h[i]-h[st[tp]]);
			tp--;
		}
		st[++tp]=i;
		int po=gt(s[i]-l,i);
		f[i]=qu(1,1,n,po+1,i);
		mod(1,1,n,i+1,f[i]+h[i+1]);
	}
	printf("%lld",f[n]);
}

[HNOI2007]梦幻岛宝珠
(瞄了一眼题解)
数据范围很奇怪,自己的做法也有点奇怪
把所有数按照\(b\)从大到小排序后dp。
\(f_i\)表示前\(i\)个数的答案。
然而这还不够,因为我们要知道背包的容量。
\(f_{i,j}\)表示前\(i\)个数,背包容量除以\(2^i\)等于\(j\)的答案。
转移先把\(f_{i,j}\)转移到\(f_{i-1,j*2+p}\),其中\(p\)\(w\)的二进制表示在\(i\)位是否是\(1\)
然后考虑添加当前重量为\(s\)的物品,\(f_{i,j-s}\)转移到\(f_{i,j}\)
发现我们要做的是01背包,不是完全背包。
所以要正着枚举\(j\)
然而\(j\)太大。
由于\(a\)很小,所以每次把\(j\)\(1000\)取最小值即可。
我发现这种做法在lg题解区只有一个人用过

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 110
int n,w,a[N],b[N],va[N],v[N],f[32][1010];
vector<int>g[N];
signed main(){
	while(scanf("%lld%lld",&n,&w)==2){
		if(n==-1)
			break;
		memset(f,-63,sizeof(f));
		memset(b,0,sizeof(b));
		memset(a,0,sizeof(a));
		for(int i=0;i<=30;i++)
			g[i].clear();
		for(int i=1;i<=n;i++){
			scanf("%lld%lld",&va[i],&v[i]);
			int x=va[i];
			while(x%2==0){
				b[i]++;
				x/=2;
			}
			a[i]=x;
			g[b[i]].push_back(i);
		}
		for(int i=30;~i;i--){
			int p=((w&(1ll<<i))>0),p1=((w&(1ll<<(i-1)))>0);
			f[i][p]=max(f[i][p],0ll);
			for(int j=0;j<g[i].size();j++){
				int x=g[i][j];
				for(int k=a[x];k<=1000;k++)
					f[i][k-a[x]]=max(f[i][k-a[x]],f[i][k]+v[x]);
			}
			if(i){
				for(int j=0;j<=1000;j++)
					f[i-1][min(1000ll,p1+j*2)]=max(f[i-1][min(1000ll,p1+j*2)],f[i][j]);
			}
		}
		int ans=0;
		for(int i=0;i<=1000;i++)
			ans=max(ans,f[0][i]);
		printf("%lld\n",ans);
	}
}

[NOI Online #1 入门组] 魔法
(自己做出)
简单题。
\(f_{i,j}\)表示走到\(i\)用了\(j\)次魔法的最小代价。
但是如果枚举出边转移有后效性。
考虑把路径划分成没有用魔法...用魔法...没有用魔法的连续段,从第一个没有用魔法的连续段把路径切开。
则后面的连续段都是用魔法....没有用魔法的。
前面没有用魔法到\(x\)的代价是最短路,可以用floyd求。
\(g_i\)表示没有用过魔法到\(i\)的代价。
\(h_{i,j}\)表示开头用了一次魔法,从\(i\)\(j\)的代价。
可以枚举\(i\)的出边指向点\(k\)求得。
\(f_{i,j}=\min(f_{k,j-1}+h_{i,k})\)
\(f_{i,1}=g_i\)
用个\((\min,+)\)矩阵快速幂就完事了。

#include<bits/stdc++.h>
using namespace std;
#define N 110
#define int long long
int n,m,k,d[N][N],e[N][N];
struct no{
	int a[N][N];
};
no operator*(no x,no y){
	no ans;
	memset(ans.a,32,sizeof(ans.a));
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			for(int k=1;k<=n;k++)
				ans.a[i][j]=min(ans.a[i][j],x.a[i][k]+y.a[k][j]);
	return ans;
}
no qp(no x,int y){
	no ans;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			ans.a[i][j]=d[i][j];
	for(;y;y>>=1,x=x*x)
		if(y&1)
			ans=ans*x;
	return ans;
}
signed main(){
	scanf("%lld%lld%lld",&n,&m,&k);
	memset(d,32,sizeof(d));
	memset(e,32,sizeof(e));
	for(int i=1;i<=n;i++)
		d[i][i]=0;
	for(int i=1;i<=m;i++){
		int x,y,z;
		scanf("%lld%lld%lld",&x,&y,&z);
		d[x][y]=z;
		e[x][y]=z;
	}
	for(int k=1;k<=n;k++)
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
				d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
	no po;
	memset(po.a,63,sizeof(po.a));
	for(int i=1;i<=n;i++)
		po.a[i][i]=0;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){
			for(int k=1;k<=n;k++)
				if(e[i][k]<=100000000000000ll){
					po.a[i][j]=min(po.a[i][j],-e[i][k]+d[k][j]);
				}
		}
	po=qp(po,k);
	printf("%lld",po.a[1][n]);
}

学校食堂
(瞄了一眼题解)
考虑dp。
一个位置给前面的限制是:前面所有数\(\leq i+b_i\)
一个数受到后面的限制等于空着的位置的\(\min(i+b_i)\)
随着空位的逐渐填满,\(\min(i+b_i)\)是递增的,且值最多是空着最小数\(+8\)
这说明能够填数的区间是滑动窗口,且\([\)后面\(+9,\inf]\)全是空的。
所以我们状压最后的数(肯定离空位最小标号很近),最小的空位\(i\)\(i\)后面\(8\)个元素的状态即可。

apio2014 beads
(自己做出)
引理:问题可以被转化为:用若干条直上直下的包含\(2\)条边的链覆盖整棵树,每条边最多只能被覆盖一次,最大被覆盖的边的价值。
证明:由于蓝边不能分裂,这说明了一条边只能被覆盖一次。
一条红边被裂掉后肯定是直上直下的。
如果得知了划分方法,可以这么构造:
把所有划分出来的链(包括红链)按照顶部深度从小到大排序。
遇到一条长度为\(2\)的链,就先插入红边后分裂。否则直接插入红边。
用dp解决这个问题。
\(f_x\)表示\(x\)节点没有向它的父亲连边的方案,\(g_{x,y}\)表示\(x\)节点向上延伸一个点,向下延伸到\(y\)的最大价值。
显然只有\(O(n)\)类可能的\(g\)
转移:\(f_x=\sum_{son}\max(f_{son},g_{son,})\)
\(g_{x,y}=\sum_{son\neq y}\max(f_{son},g_{son,})+f_y+w_{x,fa}+w_{x,y}\)
显然求出\(\sum_{son}\max(f_{son},g_{son,})\)后就能快速计算答案。
但是时间复杂度是\(O(n^2)\)的。
考虑换根,用个set维护\(\max(g_{x,})\)即可。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 500010
int n,h[N],v[N],nxt[N],w[N],f[N],mx[N],fw[N],ans,ec;
multiset<int>s[N];
void add(int x,int y,int z){
	v[++ec]=y;
	w[ec]=z;
	nxt[ec]=h[x];
	h[x]=ec;
}
void dfs(int x,int fa){
	for(int i=h[x];i;i=nxt[i])
		if(v[i]!=fa){
			fw[v[i]]=w[i];
			dfs(v[i],x);
			f[x]+=max(f[v[i]],mx[v[i]]+w[i]);
		}
	for(int i=h[x];i;i=nxt[i])
		if(v[i]!=fa)
			s[x].insert(-max(f[v[i]],mx[v[i]]+w[i])+f[v[i]]+w[i]);
	if(s[x].size())
		mx[x]=f[x]+*s[x].rbegin();
	else
		mx[x]=-1e12;
}
void rt(int x,int y){
	s[y].erase(s[y].find(-max(f[x],mx[x]+fw[x])+f[x]+fw[x]));
	f[y]-=max(f[x],mx[x]+fw[x]);
	fw[y]=fw[x];
	fw[x]=-1e12;
	if(s[y].size())
		mx[y]=f[y]+*s[y].rbegin();
	else
		mx[y]=-1e12;
	f[x]+=max(f[y],mx[y]+fw[y]);
	s[x].insert(-max(f[y],mx[y]+fw[y])+f[y]+fw[y]);
	mx[x]=f[x]+*s[x].rbegin();
}
void dd(int x,int fa){
	ans=max(ans,f[x]);
	for(int i=h[x];i;i=nxt[i])
		if(v[i]!=fa){
			rt(v[i],x);
			dd(v[i],x);
			rt(x,v[i]);
		}
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<n;i++){
		int x,y,z;
		scanf("%lld%lld%lld",&x,&y,&z);
		add(x,y,z);
		add(y,x,z);
	}
	fw[1]=1e12;
	dfs(1,0);
	dd(1,0);
	printf("%lld",ans);
}

lg3354
(看了题解)
考虑dp。
\(f_{x,i}\)表示\(x\)子树的代价被计算好,子树内有\(i\)个伐木场的代价。
然而计算当前点的代价还需要知道它上面第一个点的位置。
\(f_{x,i,y}\)表示\(x\)子树的代价被计算好,子树内有\(i\)个伐木场,上面第一个伐木场在\(y\)的代价。
转移可以对于\(i\)这一维背包,枚举当前点的每个父亲作为\(y\)

#include<bits/stdc++.h>
using namespace std;
#define N 210
#define int long long
int n,k,h[N],v[N*2],nxt[N*2],w[N*2],d[N],f[N][N][N],st[N],tp,ec,a[N],tt[N][N];
void add(int x,int y,int z){
	v[++ec]=y;
	w[ec]=z;
	nxt[ec]=h[x];
	h[x]=ec;
}
void d1(int x,int fa){
	st[++tp]=x;
	if(x!=1)
		f[x][x][1]=0;
	else
		f[x][x][0]=0;
	for(int i=1;i<tp;i++)
		f[x][st[i]][0]=(d[x]-d[st[i]])*a[x];
	for(int i=h[x];i;i=nxt[i])
		if(v[i]!=fa){
			d[v[i]]=d[x]+w[i];
			d1(v[i],x);
			memset(tt,31,sizeof(tt));
			for(int j=1;j<=tp;j++){
				for(int a=0;a<=k;a++)
					for(int b=0;b<=k-a;b++)
						tt[st[j]][a+b]=min(tt[st[j]][a+b],f[x][st[j]][a]+f[v[i]][st[j]][b]);
				for(int a=0;a<=k;a++)
					for(int b=0;b<=k-a;b++)
						tt[st[j]][a+b]=min(tt[st[j]][a+b],f[x][st[j]][a]+f[v[i]][v[i]][b]);
			} 
			for(int j=1;j<=tp;j++)
				for(int a=0;a<=k;a++)
					f[x][st[j]][a]=tt[st[j]][a];
		}
	tp--;
}
signed main(){
	memset(f,31,sizeof(f));
	scanf("%lld%lld",&n,&k);
	n++;
	for(int i=2;i<=n;i++){
		int y,z;
		scanf("%lld%lld%lld",&a[i],&y,&z);
		y++;
		add(y,i,z);
		add(i,y,z);
	}
	d1(1,0);
	printf("%lld",f[1][1][k]);
}

lg4766
(看了题解)
降智好题
考虑区间dp,设\(f_{l,r}\)表示消灭出现时间在区间\([l,r]\)的敌人的最小代价。
观察到,一个区间距离最大的敌人肯定是要被消灭的。
消灭完后,肯定当前发射时间之内所有敌人也被消灭。
考虑它的位置\(p\),则枚举发射冲击波的位置\(k\)\(f_{l,r}=\max(f_{l,k-1}+f_{k+1,r}+va_k)\)

#include<bits/stdc++.h>
using namespace std;
#define N 1000
#define int long long
int f[N][N],n,a[N],b[N],d[N],st[N],tp,ia[N],ib[N];
signed main(){
	int T;
	scanf("%lld",&T);
	while(T--){
		scanf("%lld",&n);
		memset(f,0,sizeof(f));
		tp=0;
		for(int i=1;i<=n;i++)
			scanf("%lld%lld%lld",&a[i],&b[i],&d[i]);
		for(int i=1;i<=n;i++){
			st[++tp]=a[i];
			st[++tp]=b[i];
		}
		sort(st+1,st+tp+1);
		tp=unique(st+1,st+tp+1)-st-1;
		for(int i=1;i<=n;i++){
			ia[i]=lower_bound(st+1,st+tp+1,a[i])-st;
			ib[i]=lower_bound(st+1,st+tp+1,b[i])-st;
		}
		for(int le=1;le<=tp;le++)
			for(int l=1;l<=tp;l++){
				int r=l+le-1,po=-1,mx=0;
				if(r>tp)
					continue;
				for(int k=1;k<=n;k++)
					if(l<=ia[k]&&ib[k]<=r&&mx<d[k]){
						po=k;
						mx=d[k];
					}
				f[l][r]=1e18;
				if(po==-1){
					f[l][r]=0;
					continue;
				}
				for(int k=l;k<=r;k++)
					if(ia[po]<=k&&k<=ib[po]){
						f[l][r]=min(f[l][r],f[l][k-1]+f[k+1][r]+mx);
					}
			}
		printf("%lld\n",f[1][tp]);
	}
}

购物
(瞄了一眼题解)
观察题目条件,可以发现:
如果两个区间有包含,我们把被包含的区间去掉。
把剩下的区间按照左端点从小到大排序,则第\(i\)个区间不可能和标号\(>i+2\)的区间相交。
且被包含的区间只和一个剩下的区间相交。
考虑每个被包含的区间,它事实上是钦定了一个区间内部的rgb颜色数,且这个区间内部没有其它限制。
用组合数算出方案,然后把这个区间包含的数和这个区间去掉。
考虑把整个数轴从所有被覆盖的线段的左/右端点切开。
如果我们知道了只被覆盖\(1\)次区间的选择状况,则我们可以推知被覆盖\(2\)次区间的选择状况。
考虑dp,设\(f_{i,x,y}\)表示dp到第\(i\)个只被覆盖\(1\)次的区间,现在填了\(x\)个r,\(y\)个b。
转移显然。
在转移完这个区间后,我们显然能够算出下一个覆盖\(2\)次的区间的rgb个数。
用组合数算个数后跳到下一个区间。
假设前面的rg个数分别为\(x,y\),则\(f_{i,x,y}\)赋值为方案数,并且运行上一次的dp即可。

Shopping
(自己做出)
简单题。
2018年集训队论文阐述了这道题的做法:
考虑钦定一个点被选择,把这个点定为根。
那么事实上就是个经典的dfs序树形背包问题了。
可以树分治,钦定重心被选择,做一遍树形背包。
然后把重心删除,递归每个连通块处理。

lg4728
(看了题解)
这种简单题还要看题解。。。。
\(f_i\)表示末尾为\(i\),另一个序列的最小值。
然而这还不够,我们还要知道当前序列选了多少个。
\(f_{i,j}\)表示末尾是\(i\),当前末尾是\(i\)的序列有\(j\)个数的另一个序列的最小末尾。
设最小末尾是因为贪心的想,末尾越小越好。
这样子的正确性是我们选了前\(i\)个数时,由于每个数都要被取,所以\(i\)也是必须要被取的。
转移:\(f_{i,j}=\min(f_{i,j},f_{i-1,j-1}),a_{i-1}<a_i\)表示末尾和当前划分到同一个序列内。
\(f_{i,j}=\min(f_{i,j},a_i),a_i>f_{i-1,i-j}\)表示末尾和当前划分到不同序列内。

#include<bits/stdc++.h>
using namespace std;
#define N 2010
int T,f[N][N],n,a[N];
int main(){
	scanf("%d",&T);
	while(T--){
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
			scanf("%d",&a[i]);
		memset(f,63,sizeof(f));
		f[0][0]=a[0]=-1e9;
		for(int i=1;i<=n;i++)
			for(int j=1;j<=min(n/2,i);j++){
				if(a[i]>a[i-1])
					f[i][j]=min(f[i][j],f[i-1][j-1]);
				if(a[i]>f[i-1][i-j])
					f[i][j]=min(f[i][j],a[i-1]);
			}			
		if(f[n][n/2]<1e9)
			puts("Yes!");
		else
			puts("No!");
	}
}

lg6573
(自己做出)
简单题,我秒了。
考虑分层图,把图按照除\(k\)下取整分组。
显然每组的点数最多有\(k\)个。
一条边会从\(i\)组到第\(i+1\)组。
\(f_{i,j}\)表示在第\(i\)组第\(j\)个节点的最短路。
\(f_{i+1,k}=\min(f_{i,j}+w_{j,k})\)
这事实上是\(f\)乘以一个矩阵,就是\((\min,+)\)矩阵乘法。
多组询问用线段树维护即可。

#include<bits/stdc++.h>
using namespace std;
#define N 200010
int n,m,id[N],k,q,ok;
struct no{
	int a[5][5];
}a[N],bz,b[N],ans;
no operator *(no x,no y){
	no ans;
	memset(ans.a,63,sizeof(ans.a));
	for(int i=0;i<5;i++)
		for(int j=0;j<5;j++)
			for(int k=0;k<5;k++)
				ans.a[i][j]=min(ans.a[i][j],x.a[i][k]+y.a[k][j]);
	return ans;
}
void qu(int o,int l,int r,int x,int y){
	if(r<x||y<l)
		return;
	if(x<=l&&r<=y){
		if(!ok){
			ok=1;
			ans=a[o];
		}
		else
			ans=ans*a[o];
		return;
	}
	int md=(l+r)/2;
	qu(o*2,l,md,x,y);
	qu(o*2+1,md+1,r,x,y);
}
void bd(int o,int l,int r){
	if(l==r){
		a[o]=b[l];
		return;
	}
	int md=(l+r)/2;
	bd(o*2,l,md);
	bd(o*2+1,md+1,r);
	a[o]=a[o*2]*a[o*2+1];
}
int main(){
	scanf("%d%d%d%d",&k,&n,&m,&q);
	for(int i=0;i<=n;i++)
		memset(b[i].a,63,sizeof(b[i].a));
	for(int i=1;i<=m;i++){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		b[x/k].a[x%k][y%k]=z;
	}
	bd(1,0,n/k);
	for(int i=1;i<=q;i++){
		int x,y;
		scanf("%d %d",&x,&y);
		ok=0;
		qu(1,0,n/k,x/k,y/k-1);
		if(x/k==y/k){
			puts("-1");
			continue;
		}
		if(ans.a[x%k][y%k]>1e9)
			puts("-1");
		else
			printf("%d\n",ans.a[x%k][y%k]);
	}
}

CF938F
(瞄了一眼题解)
简单题做了这么久。。。
考虑按位贪心,我们要让第一位,第二位,第三位...尽量小。
考虑我们现在枚举第\(i\)位要尽量小,那么我们考虑可能的候选位置\(j\),我们需要快速判定是否符合要求。
考虑我们上一次保留的末尾位置\(x\),我们可以得知\(x\)之前删去的长度集合。
先考虑下一次填的数的最小值,枚举位置\(z\)\(z-i\)的二进制子集\(p\)
如果\(p\)在之前合法则可以填入。
我们事实上通过这个过程也可以钦定前缀后哪些长度集合有解。
但是这样子是\(3^{\log_2n}n\)的。
考虑优化,我们事实上是判定\(j-i\)是否有合法的子集,高维前缀和即可。

#include<bits/stdc++.h>
using namespace std;
#define N 5010
char s[N];
int f[N],n,l;
int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	l=log2(n);
	int p=(1<<l);
	for(int i=0;i<p;i++)
		f[i]=1;
	for(int i=1;i<=n-p+1;i++){
		int mn=1e9;
		for(int j=0;j<p;j++)
			for(int k=0;k<l;k++)
				if(!(j&(1<<k)))
					f[j+(1<<k)]|=f[j];
		for(int j=0;j<p;j++)
			if(f[j])
				mn=min(mn,(int)s[i+j]);
		for(int j=0;j<p;j++)
			if(s[i+j]!=mn)
				f[j]=0;
		putchar(mn);
	}
}

[JSOI2016]位运算
(瞄了一眼题解)
最后的暴力部分还想用组合数计算。。。。
注意到奇怪的数据范围,所以考虑状态压缩。
把所有数\(p_1,p_2...p_n\)从小到大排序,从高到低填数。
\(f_{i,t}\)表示填了\(i\)位,如果\(t_j=0\)\(p_j=p_{j+1}\)
如果\(t_n=0\)\(p_n=s_i\),否则\(p_n\neq s_i\)
转移时,枚举每一位填的数,判定\(1\)个数是否为奇数,按照题意转移。
注意数的上限。
我们发现\(s\)是循环给出的,所以转移\(k\)次后,每次转移是相同的。
所以可以用矩阵乘法优化计算。
从itst学来另一个做法。
忽略排序和不相同的限制,可以用数位dp解决。
所有数都是一样的。
\(f_{i,j}\)表示填了前\(i\)个数,目前确定有\(j\)个符合要求。
转移比较显然。
像前面一样用矩阵乘法优化。
考虑容斥求出原问题的解,我们容斥后事实上就是枚举\(n\)个数字的集合划分。
由于出现偶数次的数的xor和是\(0\),所有出现奇数次的数的xor和也是\(0\)
假设出现奇数次的数的个数是\(s\)
事实上我们就是要求出\(s\)个数,它们都要\(<R\)且互不相同的方案。
\(0\to n\)的答案求出即可。

小Y和恐怖的奴隶主
(自己做出)
简单题,我秒了。
发现奇怪的数据范围。
\(f_{i,a,b,c}\)表示现在第\(i\)轮,有\(a\)\(3\)血随从,\(b\)\(2\)血随从,\(c\)\(1\)血随从。
转移枚举我们打的是\(3\)还是\(2\)还是\(1\)血随从,然后显然能够轻松转移。
注意到我们的转移是乘以一个矩阵,所以可以矩阵快速幂优化。
这还是太慢,用矩阵乘向量优化即可。

CF79D
(看了题解)
考虑差分,问题被转化成了:有一个集合\(s\),有一个序列\(a\)
可以选择\(a_l,a_{l+s_i}\)反转,问把\(a\)变成全\(0\)的最小代价。
不知道如何证明,我们把两个\(0\)变成两个\(1\)是不优的。
而从\(1\)变成\(0\)事实上是移动\(1\)
我们只会把两个\(1\)配对,配对的代价可以最短路。
问题转化成有\(2k\)个位置,\(i\)位置和\(j\)位置配对有\(b_{i,j}\)代价,求最小配对代价。
可以一般图最大权匹配,但是数据范围较小,显然可以状态压缩dp。
自己还是不擅长图论...

#include<bits/stdc++.h>
using namespace std;
#define N 100010
int n,k,l,vi[N],di[N],s[100][100],a[N],p[N],id[N],ct,st[N],f[N*20];
queue<int>q;
void dij(int o){
	memset(vi,0,sizeof(vi));
	memset(di,32,sizeof(di));
	di[o]=0;
	q.push(o);
	while(!q.empty()){
		int t=q.front();
		q.pop();
		for(int i=1;i<=l;i++){
			if(t>=a[i]&&!vi[t-a[i]]){
				vi[t-a[i]]=1;
				di[t-a[i]]=di[t]+1;
				q.push(t-a[i]);
			}
			if(t+a[i]<=n&&!vi[t+a[i]]){
				vi[t+a[i]]=1;
				di[t+a[i]]=di[t]+1;
				q.push(t+a[i]);
			}
		}
	}
	for(int i=1;i<=ct;i++)
		s[id[o]][i]=di[st[i]];
}
int main(){
	scanf("%d%d%d",&n,&k,&l);
	for(int i=1;i<=k;i++){
		int x;
		scanf("%d",&x);
		p[x]^=1;
	}
	for(int i=0;i<=n;i++){
		p[i]^=p[i+1];
		if(p[i]){
			id[i]=++ct;
			st[ct]=i;
		}
	}
	for(int i=1;i<=l;i++)
		scanf("%d",&a[i]);
	for(int i=0;i<=n;i++)
		if(id[i])
			dij(i);
	memset(f,63,sizeof(f));
	f[0]=0;
	for(int i=0;i<(1<<ct);i++){
		int po=-1;
		for(int j=0;j<ct;j++)
			if(i&(1<<j)){
				po=j;
				break;
			}
		if(po==-1)
			continue;
		for(int j=0;j<ct;j++)
			if((i&(1<<j))&&j!=po)
				f[i]=min(f[i],f[i^(1<<j)^(1<<po)]+s[j+1][po+1]);
	}
	if(f[(1<<ct)-1]<1e8)
		printf("%d\n",f[(1<<ct)-1]);
	else
		puts("-1");
}

lg4757
(瞄了一眼题解)
考虑dp,观察到奇怪的数据范围,设\(f_{i,j}\)表示\(i\)的子树,\(i\)连出的儿子边占用情况为\(j\)
把路径挂在lca上考虑。
我们假设选了一条路径,他会占用\(i\)的两个/一个连出边\(x,y\)
它的代价可以用前缀和快速计算。
现在,我们枚举当前节点的两个被占用的点\(x,y\),则\(f_{i,j}=\max(f_{i-{x}-{y}}+val)\)
发现我们对于每一对\(x,y\),只需要存储代价最大的就行了。

abc176F
(自己做出)
考虑dp。
观察题目性质,可以发现:假设我们现在删除到\(i\)位,则\(3i,3i+1,3i+2\)位是新插入的,前面有两位是原来的。
假设原来的位是\(x,y\),现在的是\(3i,3i+1,3i+2\),则可以发现\(3i+3,3i+4\)在这个集合内产生。
\(f_{i,x,y}\)表示\(i\)位置,原来两位是\(x,y\)的最大值,转移显然。
然而这样子太慢了。
考虑枚举\(i\)后用某种数据结构维护\(f_{i}\)\(f_{i-1}\)之间的转移。
分类讨论:
新的\(x,y\)都是原来的:不用处理,按照\(a_{3i},a_{3i+1},a_{3i+2}\)是否相同判定\(f_{i}\)是否要\(+1\)
新的\(x,y\)有一个是原来的:我们事实上要求数组的一行/一列的最大值。
新的\(x,y\)没有一个是原来的:要求整个数组的最大值,可以被拆成\(n\)次对一行/一列最大值的查询。
需要支持单点查询,单点修改。
显然可以维护一行/一列的最大值和加法标记。
由于每次每个数组只会加,不会减,所以在修改时只需要把对应行列与当前值取\(\max\)即可。

posted @ 2021-07-05 18:35  celerity1  阅读(44)  评论(0编辑  收藏  举报