DP 做题记录

里面有一些数据结构优化 DP 的题目(如 XI.),以及普通 DP,树形 DP(XVI. ~ XXII.),区间 DP(XXVII. ~ XXXIV.)等。

*I. P3643 [APIO2016]划艇

题意简述:给出序列 ai,bi,求出有多少序列 ci 满足 ci=1ci[ai,bi],同时非 1 的部分单调递增。

直到做到这题我才意识到我的 DP 水平有多菜。

注意到值域过大,于是对区间进行离散化,设离散化后的端点分别为 p1,p2,,pc。注意要将 [ai,bi] 变成 [ai,bi+1),即bi1,方便计算答案。

接下来考虑 DP:设 fi,j 表示第 i 个学校派出划艇数量在 Lj=[pj,pj+1) 之间时的方案数。

错误思路:fi,j={pos=1i1k=1j1fpos,k×(pj+1pj)[pj,pj+1)[ai,bi)0otherwise。错误原因:I. 没有考虑边界条件 & 枚举下界。II. 没有考虑在同一区间内也合法的情况。

边界条件就是 fi,0=1,并且注意 pos,k 的枚举下界应为 0

考虑在同一区间内合法的情况:不妨枚举最右端的不在区间 j 的位置 pos,那么剩下来 ipos 个位置。假设当中有 m 个位置满足可以落在区间 j 上,那么方案数就相当于从 m11Lj 个数 pj,pj+1,,pj+11 中选出 m 个数(因为位置 i 必须选所以是 m1 而不是 m1 相当于不选),即 (m+Lj1m)

综上所述,更新后的转移方程应为 fi,j={pos=0i1k=0j1fpos,k×(m+Lj1m)[pj,pj+1)[ai,bi)0otherwise。注意到后面的组合数可以 O(1) 递推((m+Ljm+1)=(m+Lj1m)×m+Ljm+1),那么使用前缀和优化(因为 m 与枚举的 k 无关,所以记 si,j=k=0jfi,k,则上面那一坨可以写成 pos=0i1spos,j1×(m+Lj1m))+ 倒序枚举 pos(实时更新 m 与组合数)即可 O(n3) 计算。

#include <bits/stdc++.h>
using namespace std;

#define ll long long

const int N=500+5;
const int mod=1e9+7;

ll n,cnt,a[N],b[N],p[N<<1];
ll g[N],iv[N];

int main(){
	cin>>n,g[0]=1;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i],iv[i]=(i==1?1:-mod/i*iv[mod%i]%mod+mod);
		p[++cnt]=a[i],p[++cnt]=b[i]+1;
	} sort(p+1,p+cnt+1),cnt=unique(p+1,p+cnt+1)-p-1;
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(p+1,p+cnt+1,a[i])-p;
		b[i]=lower_bound(p+1,p+cnt+1,b[i]+1)-p;
	} for(int i=1;i<cnt;i++){
		ll len=p[i+1]-p[i];
		for(int j=n;j;j--)
			if(a[j]<=i&&i<b[j]){
				ll f=0,m=0,C=1;
				for(int k=j;k;k--){
					if(a[k]<=i&&i<b[k])C=C*(m+len)%mod*iv[m+1]%mod,m++;
					f=(f+g[k-1]*C)%mod;
				} g[j]=(g[j]+f)%mod;
			}
	} ll ans=0;
	for(int i=1;i<=n;i++)ans=(ans+g[i])%mod;
	cout<<ans<<endl;
	return 0;
}

II. P7091 数上的树

题解

III. CF1156F Card Bag

题解

*IV. CF1542E2 Abnormal Permutation Pairs (hard version)

题解

*V. AT693 文字列

很 nb 的题目,没想出来。注意我们不关注字符的相对顺序,只关心有没有相邻的相同字符,因此考虑 DP:设 fi,j 表示前 i 个字母构成的有 j 对相邻字符的字符串个数。转移时枚举 j,当前字母分几段,以及一共插入到几对相邻字符中,然后组合数算算即可。答案即为 fn,0

时间复杂度 αn3,其中 α 是字符集大小,n 是字符总数。可是我连第一步 DP 都没想出来。

#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define pb push_back
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))

const ll mod=1e9+7;
const int N=260+5;

ll c,sum,a[N],f[N][N],C[N][N];
int main(){
	for(int i=0;i<N;i++)
		for(int j=0;j<=i;j++)
			C[i][j]=(j==0||j==i?1:C[i-1][j-1]+C[i-1][j])%mod;
	for(int i=0;i<26;i++)cin>>a[i];
	f[0][max(0ll,a[0]-1)]=1,sum=a[0];
	for(int i=1;i<26;i++){
		if(!a[i])continue;
		c++;
		for(int j=0;j<=sum;j++)
			for(int k=0;k<=a[i];k++)
				for(int l=0;l<=k;l++)
					if(l<=j){
						int nw=j-l+(a[i]-k);
						f[c][nw]=(f[c][nw]+f[c-1][j]*C[a[i]-1][k-1]%mod*C[j][l]%mod*C[sum+1-j][k-l])%mod;
					}
		sum+=a[i];
	}
	cout<<f[c][0]<<endl;
	return 0;
}

*VI. AT3859 [AGC020E] Encoding Subsets

好题。首先考虑没有子集限制怎么做:单纯设一个状态好像不好转移,因此设 fl,r​ 为不需要括号括起来的方案数,gl,r​ 为只能用括号括起来或只有单字符的方案数,那么枚举第一个括号的结尾位置,有 fl,r=k=lrgl,kfk+1,r​,gl,r=d|rl+1  d<rl+1gl,l+d1[sl,l+d1==srd+1,r]注意观察边界条件​。

有一部分我理解了很久才明白:在计算 fl,r​ 的过程中,如果第一个括号的开头不在 l​ 的位置,那么会计入 gl,lfl+1,r​ 的贡献。

但是这么做对于本题就不行了,不过我们受到上述启发,可以用字符串作为 DP 的变量。注意到字符串压缩时,只要有一个字符串的某一位为 0​​,那么最终得到的字符串的该位也只能为 0​​,比如说 01/11/10​ 只能压缩成 00​,而 011/110​ 的压缩结果就是 010​,i.e. 第二位既可以是 0​ 也可以是 1​​。然后就可以 DP 了,时间复杂度 O(can pass)​。

const int N=5e3+5;
const ll mod=998244353;

map <string,int> f,g;
int calcg(string s);
int calcf(string s){
	if(s.size()==0)return 1;
	if(s.size()==1)return s[0]-'0'+1;
	if(f.find(s)!=f.end())return f[s];
	int res=0;
	for(int i=1;i<=s.size();i++){
		string pre="",suf="";
		for(int j=0;j<i;j++)pre+=s[j];
		for(int j=i;j<s.size();j++)suf+=s[j];
		res=(res+1ll*calcg(pre)*calcf(suf))%mod;
	} return f[s]=res;
}
int calcg(string s){
	if(s.size()==0)return 1;
	if(s.size()==1)return s[0]-'0'+1;
	if(g.find(s)!=g.end())return g[s];
	int res=0;
	for(int i=1;i<s.size();i++)
		if(s.size()%i==0){
			string nw="";
			for(int j=0;j<i;j++)nw+="1";
			for(int j=0;j<s.size();j++)nw[j%i]=((nw[j%i]-'0')&(s[j]-'0'))+'0';
			res=(res+calcf(nw))%mod;
		} return g[s]=res;
}
int main(){
	string s; cin>>s,cout<<calcf(s)<<endl;
	return 0;
}

*VII. P2470 [SCOI2007]压缩

双倍经验,不过略有区别:设 fi,j​ 表示中间没有 M 的方案数(假定 i​ 前面的所有字符不在缓冲串中),gi,j​ 表示中间有 M 的方案数,那么有 fl,r=mink=lr1(fi,k+rk)(为什么是 fi,k:因为我们假定进入 [l,r] 时缓冲串为空,所以一定是前缀,如果是 fk,rlk1 的字符就在缓冲串里面了),不要忘记用 rl+1 以及 fl,mid+1(如果 rl+1 是偶数且 slmid=smid+1r)更新 fl,rg 就很简单了啊,枚举 M 出现的位置,那么,mink=lr1(min(fi,k,gi,k)+min(fk+1,r,gk+1,r)+1)​。​时间复杂度 O(n3)

const int N=50+5;
const ll mod=998244353;

char s[N];
int f[N][N][2];
bool vf[N][N][2];
bool check(int l,int r){
	if(r-l+1&1)return 0;
	for(int i=l,j=l+r+1>>1;j<=r;i++,j++)
		if(s[i]!=s[j])return 0;
	return 1;
}
int cmin(int &x,int y){if(x>y)x=y;}
int F(int l,int r,int tp){
	if(l>r)return 0;
	if(l==r)return 1;
	if(vf[l][r][tp])return f[l][r][tp];
	vf[l][r][tp]=1,f[l][r][tp]=r-l+1;
	if(tp==0){
		if(check(l,r))cmin(f[l][r][0],F(l,l+r>>1,0)+1);
		for(int i=l;i<r;i++)cmin(f[l][r][0],F(l,i,0)+r-i);
	} else for(int i=l;i<r;i++)
		cmin(f[l][r][1],min(F(l,i,0),F(l,i,1))+min(F(i+1,r,0),F(i+1,r,1))+1);
	return f[l][r][tp];
}
int main(){
	scanf("%s",s+1);
	cout<<min(F(1,strlen(s+1),0),F(1,strlen(s+1),1))<<endl;
	return 0;
}

VIII. P4302 [SCOI2003]字符串折叠

三倍经验。和 AT3859 一模一样了属于是。

const int N=100+5;
const ll mod=998244353;

char s[N];
int f[N][N][2];
bool vf[N][N][2];
int cmin(int &x,int y){if(x>y)x=y;}
int F(int l,int r,int tp){
	if(l>=r)return r-l+1;
	if(vf[l][r][tp])return f[l][r][tp];
	vf[l][r][tp]=1,f[l][r][tp]=r-l+1;
	if(tp==0)for(int i=l;i<=r;i++)cmin(f[l][r][0],F(l,i,1)+F(i+1,r,0));
	else for(int i=1;i<r-l+1;i++)if((r-l+1)%i==0){
		bool ok=1; for(int j=l+i;j<=r;j++)ok&=s[j]==s[j-i];
		if(ok)cmin(f[l][r][1],F(l,l+i-1,0)+3+((r-l+1)/i>=10));
	} return f[l][r][tp];
}
int main(){
	scanf("%s",s+1),cout<<F(1,strlen(s+1),0)<<endl;
	return 0;
}

*IX. CF67C Sequence of Balls

若没有 4 操作,有一个显然的 DP,不谈。

考虑 swap 操作究竟应该怎么用:设 BjA1i1 的最后一次出现在位置 xAiB1j1 的最后一次出现在位置 y​。​如果 x,y 至少有一个不存在,那么显然 swap 不优。否则我们删掉 Ax+1i1,swap,再在中间插入 By+1j1 即可。

正确性其实挺难说明的,结合 2teti+td 感性理解一下,时间复杂度 O(n2)

const int N=5e3+5;

int f[N][N],pres[N],pret[N];
int ti,td,tr,te;
string s,t;
int main(){
	cin>>ti>>td>>tr>>te>>s>>t; memset(f,0x3f,sizeof(f));
	for(int i=0;i<=s.size();i++,mem(pret,0,N)){
		if(i>1)pres[s[i-2]]=i-1;
		for(int j=0;j<=t.size();j++){
			if(i==0){f[i][j]=j*ti; continue;}
			if(j==0){f[i][j]=i*td; continue;}
			f[i][j]=min(f[i-1][j]+td,f[i][j-1]+ti);
			if(s[i-1]==t[j-1])f[i][j]=min(f[i][j],f[i-1][j-1]);
			else f[i][j]=min(f[i][j],f[i-1][j-1]+tr);
			int x=pres[t[j-1]],y=pret[s[i-1]];
			if(x&&y)f[i][j]=min(f[i][j],f[x-1][y-1]+td*(i-x-1)+ti*(j-y-1)+te);
			pret[t[j-1]]=j;
		}
	}
	cout<<f[s.size()][t.size()]<<endl;
	return 0;
}

*X. NOIP2021 六校联考 0820 T3 /se

题意简述:将 n 条线段划分为不超过 k 个集合,求每个集合中所有线段的并的长度之和的最大值。

kn5000lr106。时限 1s,空限 512MB。

首先,对于完全覆盖某一条线段 i 的线段 j 来说,其实我们不需要考虑它:它要么和 i 分到一组,不会造成影响,要么单独被分到一组。

因此,找到所有不完全覆盖任何其它线段的所有线段,按右端点排序,显然左端点也是单调的,所以设计 DP fi,j 表示前 i 条前段划分成了 j 个集合的答案。最后 fn,j 和完全覆盖的线段合并贡献即可。时间复杂度 O(nk)

  • Trick:实际上,对于很多线段 DP 题目,我们可以挖掘性质使得所有需要考虑的线段在右端点单调的情况下,左端点也单调
#include <bits/stdc++.h>
using namespace std;

typedef double db;
typedef long long ll;
typedef long double ld;
typedef pair <int,int> pii;
typedef unsigned long long ull;
typedef pair <long long,long long> pll;

#define fi first
#define se second
#define gc getchar()
#define pb emplace_back
#define mem(x,v,n) memset(x,v,sizeof(x[0])*n)
#define cpy(x,y,n) memcpy(x,y,sizeof(x[0])*n)

const ld Pi=acos(-1);
const ll mod=998244353;

inline ll read(){
    ll x=0; bool sign=0; char s=gc;
    while(!isdigit(s))sign|=s=='-',s=gc;
    while(isdigit(s))x=(x<<1)+(x<<3)+(s-'0'),s=gc;
    return sign?-x:x;
}

const int N=5e3+5;

int n,k,c,d;
ll ans,t[N],tt[N],pre[N],f[N][N];
struct seg{
	ll l,r;
	bool operator < (const seg &v) const{
		return r<v.r;
	}
	bool operator == (const seg &v) const{
		return l==v.l&&r==v.r;
	}
}tmp[N],s[N];

int main(){
	freopen("se.in","r",stdin);
	freopen("se.out","w",stdout);
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>tmp[i].l>>tmp[i].r;
	for(int i=1;i<=n;i++){
		tt[i]=tmp[i].r-tmp[i].l;
		bool lap=0;
		for(int j=1;j<=n;j++)
			if(tmp[i].l<=tmp[j].l&&tmp[j].r<=tmp[i].r)
				if(tmp[i]==tmp[j]&&i<j||!(tmp[i]==tmp[j])){
					lap=1; break;
				}
		if(lap)t[++d]=tmp[i].r-tmp[i].l;
		else s[++c]=tmp[i];
	}
	sort(s+1,s+c+1),mem(f,N,0xcf);
	f[0][0]=f[c][0]=0;
	for(int i=1;i<=c;i++)
		for(int j=1;j<=min(k,i);j++){
			pre[j]=max(pre[j],f[i-1][j-1]+s[i].r);
			f[i][j]=max(0ll,pre[j]-s[i].l);
		}
	sort(t+1,t+d+1),reverse(t+1,t+d+1);
	sort(tt+1,tt+n+1),reverse(tt+1,tt+n+1);
	for(int j=1;j<=min(k,c);j++){
		ll sum=f[c][j];
		for(int i=1;i<=d&&i+j<=k;i++)sum+=t[i];
		ans=max(ans,sum);
	}
	ll sum=0;
	for(int i=1;i<=k-1;i++)sum+=tt[i];
	cout<<max(ans,sum)<<endl;
	return 0;
}

*XI. NOIP2021 六校联考 0818 T2 看烟花

题意简述:给出 n 个字符串 Si,求用这些字符串的所有子串拼出 T 的最小代价,与方案数 mod998244353。使用一次 Si 的子串代价为 Ci,方案不同当且仅当 T 中存在至少一个字符满足来自不同的字符串。

|T|105n200Ci104|Si|3×104。**保证 Ci 随机生成****。时限 6s。空限 512MB。

fi,gi 分别表示拼出 T1i 的最小代价与方案数。枚举位置 i 与字符串 j,找到 Sj 子串和 T1i 的后缀最长匹配长度 l,从 fili1 转移即可,方案数同理。可以对于每个 Sj 建出 SAM,i 移动时 O(1) 更新 SjT1il。注意转移是一段区间,所以用线段树维护区间最小代价以及最小代价方案数即可做到 |T|nlog|T|,不过被卡常了,开 O3 勉强 5950ms 通过。

const int N=1e5+5;
const int S=6e4+5;
const pii inf={2e9,0};

struct node{
	int id,cst;
	bool operator < (const node &v) const {
		return cst<v.cst;
	}
}d[N];

char s[205][S];
struct SAM{
	char s[S];
	int l,cnt,las,cost,p,nwlen;
	int son[S][5],fa[S],len[S];
	void ins(char s){
		int it=s-'a',p=las,cur=++cnt;
		las=cur,len[cur]=len[p]+1;
		while(!son[p][it])son[p][it]=cur,p=fa[p];
		if(!p)return fa[cur]=1,void();
		int q=son[p][it];
		if(len[p]+1==len[q])return fa[cur]=q,void();
		int cl=++cnt;
		fa[cl]=fa[q],fa[q]=fa[cur]=cl;
		cpy(son[cl],son[q],5),len[cl]=len[p]+1;
		while(son[p][it]==q)son[p][it]=cl,p=fa[p];
	}
	void build(){
		cnt=las=1,l=strlen(s+1);
		for(int i=1;i<=l;i++)ins(s[i]);
	}
	int jump(char s){
		int it=s-'a';
		while(p){
			if(!son[p][it])p=fa[p],nwlen=len[p];
			else return p=son[p][it],++nwlen;
		} return p=1,0;
	}
}tr[205];

char t[N];
int n,len;

pii c[N<<2];
void add(int x,pii y){x++; while(x)c[x]=c[x]+y,x-=x&-x;}
pii query(int x,int r){pii s=inf; x++,r++; while(x<=r)s=s+c[x],x+=x&-x; return s;}
int main(){
	freopen("firework.in","r",stdin);
	freopen("firework.out","w",stdout);
	scanf("%s%d",t+1,&n),len=strlen(t+1);
	for(int i=1;i<=n;i++)scanf("%d%s",&d[i].cst,s[i]+1),d[i].id=i;
	sort(d+1,d+n+1);
	for(int i=1;i<=n;i++){
		tr[i].cost=d[i].cst;
		cpy(tr[i].s,s[d[i].id],S);
	}
	for(int i=1;i<=n;i++)tr[i].build(),tr[i].p=1;
	for(int i=0;i<=len+1;i++)c[i]=inf;
	add(0,{0,1});
	for(int i=1;i<=len;i++){
		pii ans=inf;
		int tmpmx=0,cur=0;
		for(int j=1;j<=n;j++){
			int mxl=tr[j].jump(t[i]);
			if(mxl<tmpmx)continue;
			pii res=query(i-mxl,i-1);
			res.fi+=tr[j].cost;
			ans=ans+res;
			cur=max(cur,mxl);
			if(tr[j].cost!=tr[j+1].cost)tmpmx=cur,cur=0;
		}
		if(i<len)add(i,ans);
		else cout<<ans.fi<<" "<<ans.se<<endl;
	}
    return 0;
}

区间查询,单点修改,且询问区间端点单调,可以用 BIT 优化。优化完变成 2500ms 了。

pii c[N<<2];void add(int x,pii y){x++; while(x)c[x]=c[x]+y,x-=x&-x;}pii query(int x,int r){pii s=inf; x++,r++; while(x<=r)s=s+c[x],x+=x&-x; return s;}

官方解法中根据 Ci 随机生成,通过按 CiSi 从小到大排序,然后若存在 j 满足Cj<Ciljli,那么 i 显然不优,可以舍去。这样就算使用线段树也可以通过了。

*XII. P2605 [ZJOI2010]基站选址

fi,j 表示对于前 i 个村庄,若第 i 个村庄必须建造基站,且至多存在 j 个基站的最小费用。转移方程为

fi,j=ci+min0k<ifk,j1+(x=k+1iwx[dk,di[dxsx,dx+sx]])

直接计算是 O(n3k) 的,无法接受。将目光聚焦在一轮 DP 中:我们维护对于每个 j<ifjfi 的贡献(除了与 j 无关的 ci)。一开始显然符合条件。考虑当 ii+1 时的变化:对于每个满足 di1dx+sx<dix,它可能没有被覆盖到。为什么说是可能呢,因为它也可以被左边的基站覆盖。找到所有这样的 x,那么所有 dy<dxsxyi 的贡献都要增加 cx,因为 x 不被覆盖。注意到 d 递增,故可以用小根堆维护 dx+sx 并每次取出 di 的元素,进行计算。显然 y 是一段前缀,那么就是区间修改,区间最值,线段树即可。

不要忘记最后再来一轮 fi,k 的 DP,因为 n 不一定必须是基站。类似上面的转移,倒过来考虑并用大根堆维护 dxsx,每次取出 di<dxsxx 加上贡献即可。时间复杂度 O(nklogn)

const int N=2e4+5;
const int inf=1e9;

int n,k,ans=inf,tot,d[N],c[N],s[N],w[N],f[N],g[N];
int val[N<<2],laz[N<<2];
void down(int x){
	if(laz[x]){
		val[x<<1]+=laz[x],val[x<<1|1]+=laz[x];
		laz[x<<1]+=laz[x],laz[x<<1|1]+=laz[x],laz[x]=0;
	}
}
void push(int x){
	val[x]=min(val[x<<1],val[x<<1|1]);
}
void build(int l,int r,int x){
	if(l==r)return val[x]=f[l],void();
	int m=l+r>>1;
	build(l,m,x<<1),build(m+1,r,x<<1|1);
	push(x),laz[x]=0;
}
void modify(int l,int r,int ql,int qr,int x,int v){
	if(ql>qr)return;
	if(ql<=l&&r<=qr)return val[x]+=v,laz[x]+=v,void();
	int m=l+r>>1; down(x);
	if(ql<=m)modify(l,m,ql,qr,x<<1,v);
	if(m<qr)modify(m+1,r,ql,qr,x<<1|1,v);
	push(x);
}
int query(int l,int r,int ql,int qr,int x){
	if(ql<=l&&r<=qr)return val[x];
	int m=l+r>>1,ans=inf; down(x);
	if(ql<=m)ans=query(l,m,ql,qr,x<<1);
	if(m<qr)ans=min(ans,query(m+1,r,ql,qr,x<<1|1));
	return ans;
}

int pre(int x){return lower_bound(d+0,d+n+1,x)-d-1;}

int main(){
	cin>>n>>k,d[0]=-inf;
	for(int i=2;i<=n;i++)cin>>d[i];
	for(int i=1;i<=n;i++)cin>>c[i];
	for(int i=1;i<=n;i++)cin>>s[i];
	for(int i=1;i<=n;i++)cin>>w[i];
	mem(f,0x3f,N),f[0]=0;
	for(int j=1;j<=k;j++){
		build(0,n,1);
		priority_queue <pii,vector<pii>,greater<pii>> q;
		for(int i=1;i<=n;i++){
			while(!q.empty()){
				pii t=q.top();
				if(t.fi>=d[i])break; q.pop();
				modify(0,n,0,pre(d[t.se]-s[t.se]),1,w[t.se]);
			}
			g[i]=query(0,n,0,i,1)+c[i];
			q.push({d[i]+s[i],i});
		} cpy(f,g,N);
	}
	priority_queue <pii> q;
	for(int i=n;~i;i--){
		while(!q.empty()){
			pii t=q.top();
			if(t.fi<=d[i])break;
			tot+=w[t.se],q.pop();
		} ans=min(ans,f[i]+tot);
		q.push({d[i]-s[i],i});
	} cout<<ans<<endl;
	return 0;
}

XIII. P2657 [SCOI2009] windy 数

一个比较简单的数位 DP。从高位往低位计算,设 fi,j,k 表示考虑到第 i 位,当前位为 j 且是否顶到了上界的 Windy 数的个数。

const int N=15;

int d[N],f[N][10][2];
int calc(int x){
	if(!x)return 0;
	int dig=0,tmp=x; mem(f,0,N);
	while(tmp)d[++dig]=tmp%10,tmp/=10;
	for(int i=1;i<=d[dig];i++)f[dig][i][i==d[dig]]=1;
	for(int i=dig-1;i;i--)
		for(int j=0;j<10;j++){
			if(j)f[i][j][0]=1;
			for(int k=0;k<10;k++){
				if(abs(j-k)<2)continue;
				f[i][j][0]+=f[i+1][k][0];
				if(d[i]==j)f[i][j][1]+=f[i+1][k][1];
				else if(j<d[i])f[i][j][0]+=f[i+1][k][1];
			}
		}
	int sum=0;
	for(int i=0;i<10;i++)sum+=f[1][i][0]+f[1][i][1];
	return sum;
}
int main(){
	int l,r; cin>>l>>r;
	cout<<calc(r)-calc(l-1)<<endl;
	return 0;
}

XIV. P4127 [AHOI2009]同类分布

枚举各位数字之和,设 fi,j,k,l 表示考虑到第 i 位,各位数字之和为 j,前 i 位模数字之和为 k 且是否达到上界。答案即为 f1,sum,0,l

时间复杂度 O(94log4n),比较卡。

#include<bits/stdc++.h>
using namespace std;

#define db double
#define ll long long
#define ld long double
#define ull unsigned long long

#define pii pair <int,int>
#define fi first
#define se second
#define pb emplace_back
#define mem(x,v,s) memset(x,v,sizeof(x[0])*(s))
#define cpy(x,y,s) memcpy(x,y,sizeof(x[0])*(s))

const int N=20;
const int S=165;

ll a,b,ans,d[N],f[N][S][S][2];
ll calc(int sum,ll x){
	if(!x)return 0;
	ll dig=0,tmp=x; mem(f,0,N);
	while(tmp)d[++dig]=tmp%10,tmp/=10;
	for(int i=1;i<=d[dig];i++)f[dig][i][i%sum][i==d[dig]]++;
	for(int i=dig-1;i;i--)
		for(int j=0;j<=sum;j++)
			for(int k=0;k<sum;k++)
				for(int l=0;l<10;l++){
					if(!j&&!k&&l)f[i][l][l%sum][0]++;
					int c=j+l,s=(k*10+l)%sum;
					f[i][c][s][0]+=f[i+1][j][k][0];
					if(l==d[i])f[i][c][s][1]+=f[i+1][j][k][1];
					else if(l<d[i])f[i][c][s][0]+=f[i+1][j][k][1];
				}
	return f[1][sum][0][0]+f[1][sum][0][1];
}
int main(){
	cin>>a>>b;
	for(int i=1;i<=162;i++)ans+=calc(i,b)-calc(i,a-1);
	cout<<ans<<endl;
	return 0;
}

XV. P4124 [CQOI2016]手机号码

fi,j,k,l,m,n 表示考虑到第 i 位,是否出现连续三个相邻数字,是否出现 48,是否顶到上界,当前连续段长 m,当前位为 n 的数字个数。DP 即可。

#include<bits/stdc++.h>
using namespace std;

#define db double
#define ll long long
#define ld long double
#define ull unsigned long long

#define pii pair <int,int>
#define fi first
#define se second
#define pb emplace_back
#define mem(x,v,s) memset(x,v,sizeof(x[0])*(s))
#define cpy(x,y,s) memcpy(x,y,sizeof(x[0])*(s))

const int N=15;

// f_{i,j,k,l,m,n} 表示第 i 位,是否出现连续三个相邻数字,
// 是否出现 4 和 8,是否顶到上界,当前连续段长 m,当前位为 n 的数字个数
ll l,r,buc[N],d[N],f[N][2][4][2][3][10];
ll calc(ll x){
	ll dig=0,tmp=x,ans=0; mem(f,0,N);
	while(tmp)d[++dig]=tmp%10,tmp/=10;
	f[dig+1][0][0][1][1][0]=1;
	for(int i=dig+1;i>1;i--)
		for(int j=0;j<2;j++)
			for(int k=0;k<3;k++)
				for(int m=0;m<3;m++)
					for(int n=0;n<10;n++){
//						if(f[i][j][k][0][m][n])cout<<i<<" "<<j<<" "<<k<<" "<<0<<" "<<m<<" "<<n<<"       "<<f[i][j][k][0][m][n]<<endl;
//						if(f[i][j][k][1][m][n])cout<<i<<" "<<j<<" "<<k<<" "<<1<<" "<<m<<" "<<n<<"       "<<f[i][j][k][1][m][n]<<endl;
						for(int u=0;u<10;u++){
							int nwk=k|buc[u],nwm=(n==u?m+1:1),nwj=j|(nwm==3);
							if(nwk==3||!u&&i==dig+1)continue;
							if(nwm==3)nwm=1;
							f[i-1][nwj][nwk][0][nwm][u]+=f[i][j][k][0][m][n];
							if(u<d[i-1])f[i-1][nwj][nwk][0][nwm][u]+=f[i][j][k][1][m][n];
							else if(u==d[i-1])f[i-1][nwj][nwk][1][nwm][u]+=f[i][j][k][1][m][n];
						}
					} 
	for(int i=0;i<3;i++)
		for(int j=0;j<2;j++)
			for(int k=0;k<3;k++)
				for(int l=0;l<10;l++)
					ans+=f[1][1][i][j][k][l];
	return ans;
}
int main(){
	buc[4]=1,buc[8]=2;
	cin>>l>>r,cout<<calc(r)-(l>1e10?calc(l-1):0);
	return 0;
}

*XVI. P4649 [IOI2007] training 训练路径

神仙题。

首先补集转化:在所有土路上设置障碍的代价 减去 不设置障碍的边的代价之和的最大值,前提是符合题意。

对于两端颜色不同的非树边可以直接去掉,因为必须选。否则固定一个根,令非树边 (u,v) 覆盖的边是原树上 u,v 之间的所有边,那么任何两个选择保留的非树边覆盖的边不能有交。

接下来就是神仙的设计 DP 环节:考虑到度数很小,可以状压。设 fi,S 表示不考虑 i 儿子集合 S 所获得的最大贡献。

对于每个节点 x,考虑所有 LCA 为 x 的非树边 (u,v)

  • 若不选 (u,v),则 fx,S=sonSfson,0
  • 若选择 (u,v),则贡献为 fu,0+fv,0 加上 uxvx 的路径上所有非端点 不考虑包含 uv 为子节点的儿子的贡献,记为 c。注意特判 uv=x 的情况。

直接这样做的复杂度是 m22S,无法承受,主要复杂度瓶颈在于求 c。但是注意到对于相同的 (u,v)c 是固定的,因此直接对于所有 S 满足 u,vS,记 i,j 分别表示包含 u,vx 的儿子,进行转移 fx,S=max(fx,S,fx,S|i|j+c) 即可。

时间复杂度 O(m2d+nm)。其中 d 为最大度数。

const int N = 1e3 + 5;
const int M = 5e3 + 5;
const int S = 10;

int c, hd[N], nxt[N << 1], to[N << 1];
void add(int u, int v) {nxt[++c] = hd[u], hd[u] = c, to[c] = v;}

struct Edge {
	int u, v, c;
} e[M];

int n, m, sum, edg, dep[N], fa[N][S];
int LCA(int x, int y) {
	if(dep[x] < dep[y]) swap(x, y);
	for(int i = S - 1; ~i; i--)
		if(dep[fa[x][i]] >= dep[y])
			x = fa[x][i];
	if(x == y) return x;
	for(int i = S - 1; ~i; i--)
		if(fa[x][i] != fa[y][i])
			x = fa[x][i], y = fa[y][i];
	return fa[x][0]; 
}
void dfs1(int id, int f){
	fa[id][0] = f, dep[id] = dep[f] + 1;
	for(int i = 1; i < S; i++) fa[id][i] = fa[fa[id][i - 1]][i - 1];
	for(int i = hd[id]; i; i = nxt[i]) {
		int it = to[i];
		if(it == f) continue;
		dfs1(it, id);
	}
}

int sid[N], rev[N], f[N][1 << S];
vector <Edge> ex[N];
void dfs2(int id) {
	for(int i = hd[id]; i; i = nxt[i]) {
		int it = to[i];
		if(it == fa[id][0]) continue;
		dfs2(it);
	}
	int son = 0;
	for(int i = hd[id]; i; i = nxt[i]) {
		int it = to[i];
		if(it == fa[id][0]) continue;
		sid[it] = 1 << son, rev[son++] = it;
	}
	assert(sid[id] == 0);
	for(int i = 0; i < 1 << son; i++)
		for(int j = 0; j < son; j++)
			if(!(i >> j & 1))
				f[id][i] += f[rev[j]][0];
	for(Edge it : ex[id]) {
		int u = it.u, v = it.v, c = it.c;
		if(u != id) {
			c += f[u][0];
			while(fa[u][0] != id)
				c += f[fa[u][0]][sid[u]], u = fa[u][0];
		}
		if(v != id) {
			c += f[v][0];
			while(fa[v][0] != id)
				c += f[fa[v][0]][sid[v]], v = fa[v][0];
		}
		for(int i = 0; i < 1 << son; i++)
			if(!(i & sid[u]) && !(i & sid[v]))
				f[id][i] = max(f[id][i], 
					f[id][i | sid[u] | sid[v]] + c);
			
	}
}
int main(){
	cin >> n >> m;
	for(int i = 1; i <= m; i++) {
		int u, v, c; cin >> u >> v >> c;
		if(c) e[++edg] = {u, v, c}, sum += c;
		else add(u, v), add(v, u);
	} dfs1(1, 0);
	for(int i = 1; i <= m; i++)
		if((dep[e[i].u] & 1) == (dep[e[i].v] & 1))
			ex[LCA(e[i].u, e[i].v)].pb(e[i]);
	dfs2(1), cout << sum - f[1][0] << endl;
	return 0;
}

XVII. BZOJ 2164 采矿

题意简述:给定一棵树,有 m 个士兵,在节点 i 处放置 j 个士兵的贡献为 fi,j。两种操作:更改所有 fi, 或给出 (u,v),保证 vu 的 祖先(可能相等),求在 u 的子树中任意节点放置任意数量士兵,且在 (u,v) 的简单路径上(不包括 u,若 u,v 相等则包括 u)最多一个节点放置任意数量士兵,同时士兵数量总和不超过 m 的最大贡献。

数据生成方法见输入格式。

n2×104m50q2×103

u 的子树做背包, (u,v) 的链上背包对应位置取 max 再合并前者即为答案。用树链剖分 + 线段树分别维护背包和对应位置 max 的两个数组即可。时间复杂度 m2nlogn+qm2logn+qmlog2n

const int N = 2e4 + 5;
const int M = 50 + 5;
const int X = 1 << 16;
const int Y = (1ll << 31) - 1;
 
void cmax(int &x, int y) {if(x < y) x = y;}
int cnt, hd[N], nxt[N << 1], to[N << 1];
void add(int u, int v) {
    nxt[++cnt] = hd[u], hd[u] = cnt;
    to[cnt] = v;
}
 
int n, m, C, A, B, Q;
int GetInt() {
    A = ((A xor B) + (B / X) + 1ll * B * X) & Y;
    B = ((A xor B) + (A / X) + 1ll * A * X) & Y;
//  cout << A << " " << B << " " << (A ^ B) << " " << Y << endl;
    return (A xor B) % Q;
}
 
struct Knapsack {
    int a[M];
    void clear() {
        mem(a, 0, M);
    }
    void read() {
        for(int i = 1; i <= m; i++) a[i] = GetInt();
        sort(a + 1, a + m + 1);
//      cout << "check : " << endl;
//      for(int i = 1; i <= m; i++) cout << a[i] << " "; cout << endl;
    }
    void Getmax(Knapsack x) {
        for(int i = 1; i <= m; i++) cmax(a[i], x.a[i]);
    }
    void Merge(Knapsack x) {
        static int f[M]; mem(f, 0, M);
        for(int i = 0; i <= m; i++)
            for(int j = 0; j <= m - i; j++)
                cmax(f[i + j], a[i] + x.a[j]);
        cpy(a, f, M);
    }
} a[N], val[N << 2], mx[N << 2], ansv, ansm;
 
// Tree Partition
int dnum, dep[N], sz[N], fa[N], son[N], top[N], rev[N], dfn[N];
void dfs1(int id) {
    dep[id] = dep[fa[id]] + 1, sz[id] = 1;
    for(int i = hd[id]; i; i = nxt[i]) {
        int it = to[i];
        dfs1(it), sz[id] += sz[it];
        if(sz[it] > sz[son[id]]) son[id] = it;
    }
}
void dfs2(int id, int tp) {
    top[id] = tp, dfn[id] = ++dnum, rev[dnum] = id;
    if(son[id]) dfs2(son[id], tp);
    for(int i = hd[id]; i; i = nxt[i]) {
        int it = to[i];
        if(it == son[id]) continue;
        dfs2(it, it);
    }
}
 
// SegTree
void push(int x) {
    val[x] = val[x << 1], val[x].Merge(val[x << 1 | 1]);
    mx[x] = mx[x << 1], mx[x].Getmax(mx[x << 1 | 1]);
}
void build(int l, int r, int x) {
    if(l == r) return val[x] = mx[x] = a[rev[l]], void();
    int m = l + r >> 1;
    build(l, m, x << 1);
    build(m + 1, r, x << 1 | 1);
    push(x);
}
void modify(int l, int r, int p, int x) {
    if(l == r) return val[x] = mx[x] = a[rev[p]], void();
    int m = l + r >> 1;
    if(p <= m) modify(l, m, p, x << 1);
    else modify(m + 1, r, p, x << 1 | 1);
    push(x);
}
void query1(int l, int r, int ql, int qr, int x) {
    if(ql <= l && r <= qr) return ansv.Merge(val[x]), void();
    int m = l + r >> 1;
    if(ql <= m) query1(l, m, ql, qr, x << 1);
    if(m < qr) query1(m + 1, r, ql, qr, x << 1 | 1);
}
void query2(int l, int r, int ql, int qr, int x) {
    if(ql <= l && r <= qr) return ansm.Getmax(mx[x]), void();
    int m = l + r >> 1;
    if(ql <= m) query2(l, m, ql, qr, x << 1);
    if(m < qr) query2(m + 1, r, ql, qr, x << 1 | 1);
}
 
int main() {
    cin >> n >> m >> A >> B >> Q;
    for(int i = 2; i <= n; i++) cin >> fa[i], add(fa[i], i);
    dfs1(1), dfs2(1, 1);
    for(int i = 1; i <= n; i++) a[i].read();
    cin >> C, build(1, n, 1);
    for(int i = 1; i <= C; i++) {
        int op, u, v; cin >> op >> u;
        if(op == 0) {
            a[u].read(), modify(1, n, dfn[u], 1);
            continue;
        } cin >> v;
        ansv.clear(), ansm.clear();
        query1(1, n, dfn[u], dfn[u] + sz[u] - 1, 1);
        if(u == v) {
            printf("%d\n", ansv.a[m]);
            continue;
        } u = fa[u];
        while(top[u] != top[v]) {
            query2(1, n, dfn[top[u]], dfn[u], 1);
            u = fa[top[u]];
        } query2(1, n, dfn[v], dfn[u], 1);
        ansv.Merge(ansm), printf("%d\n", ansv.a[m]);
    }
    return 0;
}

*XVIII. P3262 [JLOI2015]战争调度

注意到对于一个子节点,我们无法确定它的贡献,因为我们不知道它的父亲的状态。但是由于是完全二叉树,我们可以爆搜:对于叶子结点,它的时间复杂度为 O(n)。而根据主定理,T(n)=4T(n2)+f(n2),解得 T(n)=Θ(n2logn)

故时间复杂度为 O(4n1(n1))

const int N = 10;
void cmax(int &x, int y) {if(y > x) x = y;}

int n, m, g[1 << N][1 << N];
int type[N], w[1 << N][N], f[1 << N][N];
// type = 1 : war
void dfs(int id, int dep) {
	if(dep == 0) {
		g[id][0] = g[id][1] = 0;
		for(int i = 1; i < n; i++)
			if(type[id >> i] == 1) g[id][1] += w[id][i];
			else g[id][0] += f[id][i];
		return;
	}
	type[id] = 1, dfs(id << 1, --dep), dfs(id << 1 | 1, dep);
	int sz = 1 << dep;
	for(int i = 0; i <= sz << 1; i++) g[id][i] = 0;
	for(int i = 0; i <= sz; i++)
		for(int j = 0; j <= sz; j++)
			cmax(g[id][i + j], g[id << 1][i] + g[id << 1 | 1][j]);
	type[id] = 2, dfs(id << 1, dep), dfs(id << 1 | 1, dep);
	for(int i = 0; i <= sz; i++)
		for(int j = 0; j <= sz; j++)
			cmax(g[id][i + j], g[id << 1][i] + g[id << 1 | 1][j]);
}
int main(){
	cin >> n >> m;
	for(int i = 1 << n - 1; i < 1 << n; i++)
		for(int j = 1; j < n; j++)
			cin >> w[i][j];
	for(int i = 1 << n - 1; i < 1 << n; i++)
		for(int j = 1; j < n; j++)
			cin >> f[i][j];
	dfs(1, n - 1);
	int ans = 0;
	for(int i = 0; i <= m; i++) cmax(ans, g[1][i]);
	cout << ans << endl;
	return 0;
}

*XIX. SP3734 PERIODNI - Periodni

首先,显然的是一列最多放一个数。我们设 fi,j 表示第 i 列所在连续段放 j 个的复杂度(一开始 fi,0=1,fi,j=0 (j>0))。注意到对于当前序列中最大的 hi,找到 hi1hi+1 中较大的那一个,记为 hj,然后切掉 hi 这个连续段最上面 hihj 的高度并和 hj 合并。切掉一段高度就是背包合并:枚举原来 fifj 放的棋子个数 x,y,再枚举在 fi 连续段 hj+1hi 高度区间新填了 k 个棋子,有

fi,x+y+k=x=0sziy=0szjk=0szixfi,xfj,yAkhihj(szixk)

根据树形背包的复杂度分析,枚举 x,y 的复杂度为 n2。因此总时间复杂度为 O(n3)

const int N = 500 + 5;
const int H = 1e6 + 5;
const ll mod = 1e9 + 7;

void add(ll &x, ll y) {x = (x + y) % mod;}

ll ksm(ll a, ll b) {
	ll s = 1;
	while(b) {
		if(b & 1) s = s * a % mod;
		a = a * a % mod, b >>= 1;
	} return s;
}
ll fc[H], ifc[H];
ll A(int n, int m) {return fc[n] * ifc[n - m] % mod;}
ll C(int n, int m) {return fc[n] * ifc[m] % mod * ifc[n - m] % mod;}

ll n, k, ans, h[N], f[N][N], sz[N];
int stc[N], top;
vector <int> id;

// merge x to y
void merge(int x, int y) {
	static ll g[N]; mem(g, 0, N);
	ll dif = h[x] - h[y];
	for(int i = 0; i < sz[x]; i++)
		for(int k = 0; k <= min(sz[x] - i, dif); k++)
			for(int j = 0; j < sz[y]; j++)
				add(g[i + j + k], f[x][i] * f[y][j] % mod * 
					C(sz[x] - i, k) % mod * A(dif, k));
	cpy(f[y], g, N), sz[y] += sz[x];
}

int main(){
	cin >> n >> k, fc[0] = ifc[0] = 1;
	for(int i = 1; i < H; i++) fc[i] = fc[i - 1] * i % mod;
	ifc[H - 1] = ksm(fc[H - 1], mod - 2);
	for(int i = H - 2; i; i--) ifc[i] = ifc[i + 1] * (i + 1) % mod;
	for(int i = 1; i <= n; i++)
		id.pb(i), sz[i] = f[i][0] = 1, cin >> h[i];
	while(id.size() > 1) {
		ll mxh = 0, p = -1, mer;
		for(int it : id) mxh = max(mxh, h[it]);
		for(int i = 0; i < id.size(); i++)
			if(h[id[i]] == mxh) p = i;
		if(p == 0) mer = 1;
		else if(p == id.size() - 1) mer = id.size() - 2;
		else if(h[id[p - 1]] > h[id[p + 1]]) mer = p - 1;
		else mer = p + 1;
		merge(id[p], id[mer]), id.erase(id.begin() + p);
	}
	int res = id[0];
	for(int i = 0; i <= k; i++) if(h[res] >= k - i)
		add(ans, f[res][i] * C(n - i, k - i) % mod * A(h[res], k - i));
	cout << ans << endl; 
	return 0;
}

XX. P6419 [COCI2014-2015#1] Kamp

注意到对于一个点 u 的答案就是从 u 到所有举行聚会的地方的路径的并的总长度乘 2 减去 u 到关键节点的路径长度最大值。于是换根 DP 即可。

每次向下传 maxdis 的时候,若子节点是当前节点的 maxdis 来源,那么就要下传次大值。可以记录最大值,最大值来源,次大值,次大值来源,或者直接 sort。

前者线性,后者线性对数。

需要注意子节点的子树关键点的个数为 0n 时更新路径并总长。

const int N=5e5+5;

ll cnt,hd[N<<1],nxt[N<<1],to[N<<1],val[N<<1];
void add(int u,int v,int w){nxt[++cnt]=hd[u],hd[u]=cnt,to[cnt]=v,val[cnt]=w;}

ll n,k,buc[N],sz[N];
ll sumd[N],mxd[N],ans[N];
void dfs1(int id,int f){
	sz[id]+=buc[id];
	for(int i=hd[id];i;i=nxt[i]){
		int it=to[i];
		if(it==f)continue;
		dfs1(it,id),sz[id]+=sz[it];
		if(sz[it])
			mxd[id]=max(mxd[id],mxd[it]+val[i]),sumd[id]+=sumd[it]+val[i];
	}
}
struct check{
	ll dist,id;
	bool operator < (const check &v) const {
		return dist>v.dist;
	}
};

void dfs2(int id,int f){
	
	vector <check> c; int deg=0;
	for(int i=hd[id];i;i=nxt[i])deg++;
	c.resize(deg); int p=0;
	for(int i=hd[id];i;i=nxt[i])
		if(sz[id]!=k||to[i]!=f)c[p++]={mxd[to[i]]+val[i],to[i]};
	sort(c.begin(),c.begin()+deg);
	
	ans[id]=sumd[id]*2-c[0].dist;
	
	for(int i=hd[id];i;i=nxt[i]){
		int it=to[i];
		if(it==f)continue;
		sumd[it]=sumd[id]+(sz[it]==0?val[i]:sz[it]==k?-val[i]:0);
		ll tmp=mxd[id];
		if(it==c[0].id)mxd[id]=c[1].dist;
		else mxd[id]=c[0].dist;
		dfs2(it,id);
		mxd[id]=tmp;
	}
}
int main(){
	cin>>n>>k;
	for(int i=1;i<n;i++){
		int x=read(),y=read(),z=read();
		add(x,y,z),add(y,x,z);
	}
	for(int i=1;i<=k;i++)buc[read()]=1;
	dfs1(1,0),dfs2(1,0);
	for(int i=1;i<=n;i++)printf("%lld\n",ans[i]);
	cout<<endl;
	return 0;
}

*XXI. P3757 [CQOI2017]老C的键盘

fi,j 表示 i 在以 i 为根的子树排名为 j 的排列方案数,对于一条边 (i,s),枚举 i 在已经考虑的 i 的子树(包括 i)中的排名 xjj 的子树的排名 y 以及 i 新的排名 k转移后更新 szi

wi,s 是大于号,则有:

k[x+y,szi+szj], fi,k=x=1sziy=1szjfi,xfj,y(k1x)(szi+szjkszix)

否则将 k 的枚举范围改成 [x,x+y1] 即可,因为 i<j,故最多大于 <jy1 个节点。

树形背包再枚举一维,时间复杂度 O(n3)

const int N=100+5;
const ll mod=1e9+7;

int cnt,hd[N<<1],nxt[N<<1],to[N<<1];
char val[N<<1];
void add(int u,int v,char w){
	nxt[++cnt]=hd[u],hd[u]=cnt,to[cnt]=v,val[cnt]=w;
}

ll n,sz[N],f[N][N],c[N][N],g[N]; // f i j 表示以 i 为根有 j 个数小于 h_i
void add(ll &x,ll y){x=(x+y)%mod;}
void dfs(int id){
	sz[id]=1,f[id][1]=1;
	for(int t=hd[id];t;t=nxt[t]){
		int it=to[t],lim; dfs(it),lim=sz[id]+sz[it];
		static ll g[N]; mem(g,0,N);
		for(int i=sz[id];i;i--)
			for(int j=sz[it];j;j--){
				ll coef=f[id][i]*f[it][j]%mod;
				if(val[t]=='>') for(int k=lim;k>=i+j;k--)
					add(g[k],coef*c[k-1][i-1]%mod*c[lim-k][sz[id]-i]);
				else for(int k=i+j-1;k>=i;k--)
					add(g[k],coef*c[k-1][i-1]%mod*c[lim-k][sz[id]-i]);
			}
		cpy(f[id],g,N),sz[id]=lim;
	}
}
int main(){
	cin>>n;
	for(int i=0;i<=n;i++)
		for(int j=0;j<=i;j++)
			c[i][j]=(j==0||j==i?1:(c[i-1][j-1]+c[i-1][j]))%mod;
	for(int i=2;i<=n;i++){
		char s; cin>>s;
		add(i/2,i,s);
	} dfs(1);
	ll ans=0;
	for(int i=1;i<=n;i++)add(ans,f[1][i]);
	cout<<ans<<endl;
	return 0;
}

*XXII. P4099 [HEOI2013]SAO

XXI. 的究极加强版,注意到后面的组合数与 y 无关所以使用前缀和优化:枚举 x,k,算出合法的 y 的范围即可。

需要特别注意 k 的枚举范围:单次合并 (szi+szj)szjO(n3) 的,但 sziszj 就是 O(n2)。观察上面一题的 k 的枚举范围,发现都不超过 szj,因此时间复杂度正确。

const int N=1000+5;
const ll mod=1e9+7;

int cnt,hd[N<<1],nxt[N<<1],to[N<<1];
char val[N<<1];
void add(int u,int v,char w){
	nxt[++cnt]=hd[u],hd[u]=cnt,to[cnt]=v,val[cnt]=w;
}

ll n,sz[N],f[N][N],c[N][N]; // f i j 表示以 i 为根有 j 个数小于 h_i
void add(ll &x,ll y){x=(x+y)%mod;}
void dfs(int id,int fa){
	sz[id]=1,f[id][1]=1;
	for(int t=hd[id];t;t=nxt[t]){
		int it=to[t],lim;
		if(it==fa)continue;
		dfs(it,id),lim=sz[id]+sz[it];
		static ll g[N],pre[N]; mem(g,0,N),mem(pre,0,N);
		for(int i=1;i<=sz[it];i++)pre[i]=(pre[i-1]+f[it][i])%mod;
		for(int i=sz[id];i;i--){
			if(val[t]=='>')
				for(int k=i;k<i+sz[it];k++)
					add(g[k],c[k-1][i-1]*c[lim-k][sz[id]-i]%mod*
						f[id][i]%mod*(pre[sz[it]]-pre[k-i]+mod));
			else
				for(int k=i+1;k<=i+sz[it];k++)
					add(g[k],c[k-1][i-1]*c[lim-k][sz[id]-i]%mod*
						f[id][i]%mod*pre[k-i]);
		}
		cpy(f[id],g,N),sz[id]=lim;
	}
}
void solve(){
	cin>>n,mem(f,0,N),cnt=0,mem(hd,0,N);
	for(int i=2;i<=n;i++){
		int x,y; char s; cin>>x>>s>>y,x++,y++;
		add(x,y,s),add(y,x,s=='>'?'<':'>');
	} dfs(1,0);
	ll ans=0;
	for(int i=1;i<=n;i++)add(ans,f[1][i]);
	cout<<ans<<endl;
}
int main(){
	for(int i=0;i<N;i++)
		for(int j=0;j<=i;j++)
			c[i][j]=(j==0||j==i?1:(c[i-1][j-1]+c[i-1][j]))%mod;
	int T; cin>>T;
	while(T--)solve();
	return 0; 
}

*XXIII. CF1327F AND Segments

怎么这么像 NOI 2020 D1T2。

好题。首先题目对于每一位是独立的,那么分别考虑所有限制。可以预处理出 pi 表示在第 i 为在它左边的第一个 0 最左能放到哪。设 fi,j 表示位置 i 最右边一个 0 的位置为 j 时的方案。

  • j<pifi,j=0
  • pij<ifi,j=fi1,j,这表示位置 i1
  • j=i,当 i 必须填 1fi,i=0,否则为 k=pii1fi1,k

注意到 pi 是单调的,所以直接维护 s=j=pii1fi1,j 即可,时间复杂度 O((n+m)k)

const int N = 5e5 + 5;
const int mod = 998244353;

struct Limit {
	int l, r, x;
} c[N];
int n, k, m, ans = 1;
int f[N], pos[N], buc[N];

int main() {
	cin >> n >> k >> m;
	for(int i = 1; i <= m; i++)
		scanf("%d %d %d", &c[i].l, &c[i].r, &c[i].x);
	for(int i = 0, s; i < k; i++) {
		mem(f, 0, N), mem(pos, 0, N), mem(buc, 0, N), f[0] = s = 1;
		for(int j = 1; j <= m; j++)
			if(c[j].x >> i & 1) buc[c[j].l]++, buc[c[j].r + 1]--;
			else pos[c[j].r + 1] = max(pos[c[j].r + 1], c[j].l);
		for(int j = 1; j <= n; j++) buc[j] += buc[j - 1];
		for(int j = 1; j <= n; j++) {
			pos[j] = max(pos[j], pos[j - 1]);
			for(int k = pos[j - 1]; k < pos[j]; k++) s = (s + mod - f[k]) % mod;
			if(!buc[j]) f[j] = s; s = (f[j] + s) % mod;
		} for(int k = pos[n]; k < max(pos[n], pos[n + 1]); k++) s = (s + mod - f[k]) % mod;
		ans = 1ll * ans * s % mod;
	} cout << ans << endl;
	return 0;
}

*XXIV. 2020 联考模拟知临 食堂计划

题意简述:给出一张带边权的图,求有多少条 ST 的最短路无序对 (P1,P2) 满足不存在 iS,T 使得 iP1,P2

n2×103m3×104

首先求出所有可能在最短路上的边(有向),设它们的边导出子图为 G,不难发现这是一个 DAG。nm 求出 fi,j 表示在 G 上从 ij上路径条数。

DAG 上计数应考虑容斥,设 gi 表示从 Si 的答案(不包括 P1P2 相同的情况,即 P1=P2={Si}),显然 gS=0。考虑补集转化,求出总共有多少对 Si 的无序最短路对减去恰好在节点 j 重合的无序最短路对个数,因此我们有:

gi=(fs,i2)jgjfj,i2

其中 j 的拓扑序小于 i。注意由于不计入 P1=P2 的情况,所以若 Sj 直接连边则 gi 还需减去 (fj,i2),目的是为了减去 Sji 的无序路径对:由于 Sj 是相同的,所以不同的无序对共有 (fj,i2) 个。

最后若 ST 直接连边则将答案再加上 1 即可。时间复杂度 O(nm)

fi,j 时不加判断 fi,j>0 的剪枝会 TLE,很离谱。

const int N = 2e3 + 5;
const int M = 3e4 + 5;
const int mod = 1e9 + 9;
void add(int &x, int y) { x = (x + y) % mod; }

int e[N][N], u[M], v[M], w[M];
int cnt, hd[N], nxt[M << 1], to[M << 1], val[M << 1];
void add(int u, int v, int w) {
    nxt[++cnt] = hd[u], hd[u] = cnt;
    to[cnt] = v, val[cnt] = w;
}

int n, m, S, T, L, vis[N], disS[N], disT[N], pa[N];
void Dijkstra(int S, int *dis, int *p) {
    priority_queue<pii, vector<pii>, greater<pii>> q;
    mem(dis, 63, N), q.push({ dis[S] = 0, S });
    int cnt = 0;
    while (!q.empty()) {
        pii t = q.top();
        q.pop();
        int id = t.se, d = t.fi;
        if (vis[id])
            continue;
        vis[id] = 1, p[++cnt] = id;
        for (int i = hd[id]; i; i = nxt[i]) {
            int it = to[i], v = d + val[i];
            if (dis[it] > v)
                q.push({ dis[it] = v, it });
        }
    }
    mem(vis, 0, N);
}

int p[N][N], ans[N], have[N];

int main() {
    freopen("dining.in", "r", stdin);
    freopen("dining.out", "w", stdout);
    cin >> n >> m >> S >> T, mem(e, 63, N);
    for (int i = 1; i <= m; i++) {
        u[i] = read(), v[i] = read(), w[i] = read();
        e[u[i]][v[i]] = e[v[i]][u[i]] = min(e[u[i]][v[i]], w[i]);
    }
    for (int i = 1; i <= m; i++)
        if (e[u[i]][v[i]] == w[i])
            e[u[i]][v[i]] = mod, add(u[i], v[i], w[i]), add(v[i], u[i], w[i]);

    // Dijkstra & BFS
    Dijkstra(T, disT, pa), Dijkstra(S, disS, pa);
    assert(disT[S] == disS[T]), L = disT[S];
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        p[i][i] = 1;
        static int buc[N];  mem(buc, 0, N);
        for (int j = 1; j <= n; j++) {
            int id = pa[j]; buc[id] = 1;
            if(!p[i][id]) continue;
            for (int k = hd[id]; k; k = nxt[k]) {
                int it = to[k];
                if (disS[id] + disT[it] + val[k] == L)
                    p[i][it] = (p[i][it] + p[i][id]) % mod;
            }
        }
    }

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            if (u[j] == S && v[j] == i || u[j] == i && v[j] == S) {
                if (w[j] == disS[i]) {
                    have[i] = 1;
                    break;
                }
            }

    // Dynamic Programming for answer
    for (int i = 2; i <= n; i++) {
        int id = pa[i];
        if (disS[id] + disT[id] != L)
            continue;
        ans[id] = 1ll * p[S][id] * (p[S][id] - 1) / 2 % mod;
        for (int j = 2; j < i; j++) {
            int it = pa[j];
            add(ans[id], mod - 1ll * p[it][id] * p[it][id] % mod * ans[it] % mod);
            if (have[it])
                add(ans[id], mod - 1ll * p[it][id] * (p[it][id] - 1) / 2 % mod);
        }
    }
    cout << (ans[T] + have[T]) % mod << endl;
    return 0;
}

*XXV. P2051 [AHOI2009]中国象棋

乍一眼看起来很难,因此要挖掘性质:不妨一行一行地放,不难发现每一列是等价的。因此设 fi,j,k 表示前 i 行有 j 个放了一个象棋,k 个放了两个象棋的列,转移如下:

  • 不摆象棋:fi,j,kfi+1,j,k
  • 在空列摆一个象棋:fi,j,k×(mjk)fi+1,j+1,k
  • 在一个象棋的列摆一个象棋:fi,j,k×jfi+1,j1,k
  • 在空列摆两个象棋:fi,j,k×(mjk2)fi+1,j+2,k
  • 在空列和一个象棋的列各摆一个象棋:fi,j,k×(mjk)×jfi+1,j,k+1
  • 在一个象棋的列摆两个象棋:fi,j,k×(j2)fi+1,j2,k+2

时间复杂度 O(n3)。初始值 f0,0=1

const int N = 100 + 5;
const ll mod = 9999973;

int n, m, ans, f[N][N];
void add(int &x, ll y) {x = (x + y) % mod;}
int b(int x) {return x * (x - 1) / 2;}
int main() {
	cin >> n >> m, f[0][0] = 1;
	for(int i = 1; i <= n; i++) {
		static int g[N][N];
		mem(g, 0, N);
		for(int j = 0; j <= m; j++)
			for(int k = 0; k <= m - j; k++)
				if(f[j][k]) {
					add(g[j][k], f[j][k]);
					if(j + k + 1 <= m) add(g[j + 1][k], 1ll * f[j][k] * (m - j - k));
					if(j) add(g[j - 1][k + 1], 1ll * f[j][k] * j);
					if(j + k + 2 <= m) add(g[j + 2][k], 1ll * f[j][k] * b(m - j - k));
					if(j) add(g[j][k + 1], 1ll * f[j][k] * (m - j - k) * j);
					if(j >= 2) add(g[j - 2][k + 2], 1ll * f[j][k] * b(j));
				} cpy(f, g, N);
	} for(int i = 0; i <= m; i++)
		for(int j = 0; j <= m; j++)
			add(ans, f[i][j]);
	cout << ans << endl;
	return 0;
}

*XXVI. P3147 [USACO16OPEN]262144

很有趣的题。注意到数的值域只有 40,不难想到将其作为 DP 的一维:设 fi,j 表示从 i 开始能够合成 j 的右端点(若为 0 则不存在),类似倍增进行 DP 即可。时间复杂度 O(n(V+logn))

const int N = 1 << 18;

int n, ans, f[N][60];
int main() {
	cin >> n;
	for(int i = 1; i <= n; i++) f[i][read()] = i + 1;
	for(int i = 1; i <= 58; i++)
		for(int j = 1; j <= n; j++) {
			if(!f[j][i]) f[j][i] = f[f[j][i - 1]][i - 1];
			if(f[j][i]) ans = i;
		} cout << ans << endl;
	return 0;
}

*XXVII. P5336 [THUSC2016]成绩单

神仙区间 DP。

一开始想假了,设 fi,j 表示发完 i,j 的最小代价,转移是枚举 p,q,发完 [l,p][q,r] 并从 fp+1,q1 转移过来。但实际上这是错的,因为可能发成绩单的情况是这样的:331113322233,考虑不到。

重要思想:注意到人数很少,所以不妨先对成绩大小进行离散化,然后将其也设计在 DP 方程中

那么设 gi,j,x,y 表示区间 [i,j] 未被发放的成绩最大值为 y,最小值为 x,那么原来的 fi,j 可以用 min1xyngi,j,x,y+a+b(yx) 算出。有转移方程:

gl,r,x,y=mink=lr1min(gl,k,x,y+gk+1,r,x,y,gl,k,x,y+fk+1,r,fl,k+gk+1,r,x,y)

初始值 gi,i,x,y=0 (xaiy)fi,i=a,时间复杂度 O(n5)

为什么没有想到解法:没有接触过值域设计在 DP 方程中的题目,以后一定要注意。

const int N = 50 + 5;
const int inf = 1e9;

int n, a, b, ans = 1e9, w[N], d[N], f[N][N][N][N], g[N][N];
int sq(int x) {return x * x;}
void cmin(int &x, int y) {if(x > y) x = y;}
int main() {
	cin >> n >> a >> b, mem(f, 63, N), mem(g, 63, N);
	for(int i = 1; i <= n; i++) cin >> w[i], d[i] = w[i];
	sort(d + 1, d + n + 1);
	for(int i = 1; i <= n; i++) w[i] = lower_bound(d + 1, d + n + 1, w[i]) - d, cerr << w[i] << endl;
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= w[i]; j++)
			for(int k = w[i]; k <= n; k++)
				f[i][i][j][k] = 0, g[i][i] = a;
	for(int len = 2; len <= n; len++)
		for(int l = 1, r = len; r <= n; l++, r++) {
			for(int x = 1; x <= n; x++)
				for(int y = x; y <= n; y++) {
					for(int k = l; k < r; k++)
						cmin(f[l][r][x][y], min(f[l][k][x][y] + f[k + 1][r][x][y],
							min(f[l][k][x][y] + g[k + 1][r], g[l][k] + f[k + 1][r][x][y]))),
						cmin(g[l][r], f[l][r][x][y] + a + b * sq(d[y] - d[x]));
				}
		}
	cout << g[1][n] << endl;
	return 0;
}

*XXVIII. P2339 [USACO04OPEN]Turning in Homework G

神仙区间 DP。考虑到如果从小区间转移到大区间是难以考虑的,于是我们借用 “关路灯” 的 DP 方法:从大区间转移到小区间

fl,r,0/1 分别表示 Bessie 在 [l,r] 的左端点和右端点,且 [l,r] 之外的作业全部交完的最小时间,初始值 f0,h,0=0f0,h,h=h。需要注意令 h=maxXi 否则只有 64 分。转移是 trivial 的,不懂可以看代码,这里就略去了。

时间复杂度 O(n2)

const int N = 1e3 + 5;

int ans = 1e9 + 7, c, h, b, rm, f[N][N][2], p[N];
void cmin(int &x, int y) {if(x > y) x = y;}
void cmax(int &x, int y) {if(x < y) x = y;}
int main() {
	cin >> c >> h >> b, h = 0;
	for(int i = 1; i <= c; i++) {
		int x, t; cin >> x >> t;
		p[x] = max(p[x], t), h = max(h, x);
	} mem(f, 63, N);
	f[0][h][0] = 0, f[0][h][1] = h;
	for(int len = h; len; len--)
		for(int l = 0, r = len; r <= h; l++, r++) {
			cmax(f[l][r][0], p[l]), cmax(f[l][r][1], p[r]);
			cmin(f[l + 1][r][0], f[l][r][0] + 1);
			cmin(f[l + 1][r][1], f[l][r][0] + r - l);
			cmin(f[l][r - 1][1], f[l][r][1] + 1);
			cmin(f[l][r - 1][0], f[l][r][1] + r - l);
		}
	for(int i = 0; i <= h; i++)
		ans = min(ans, max(max(f[i][i][0], f[i][i][1]), p[i]) + abs(b - i));
	cout << ans << endl;
	return 0;
}

*XXIX. BZOJ4350 括号序列再战猪猪侠

仍然是神仙区间 DP。

我们称 [a,b][c,d] 有限制当且仅当存在 e[a,b]f[c,d] 使得 matche<matchf。这个可以通过预处理二维前缀和做到。

fi,j 表示第 lr 个左括号都匹配完成且形成一段区间的方案数,有以下三种转移:

  • l 个左括号匹配的右括号放在区间 [l+1,r] 右边。fl,rfl+1,r。前提是 [l,l][l+1,r] 没有限制。
  • l 个左括号匹配的右括号放在它右边,fl,rfl+1,r。前提是 [l+1,r][l,l] 没有限制。
  • l 个左括号匹配的右括号放在区间 [l+1,k] (l<k<r) 右边,fl,rfl+1,k×fk+1,r。前提是 [l,l][l+1,k] 没有限制 且 [k+1,r][l,k] 没有限制。

时间复杂度 O(n3),初始值 fi,i=1。注意特判若 ai=bi 需要直接输出 0

为什么没有想到正解:不知道怎么处理 ai,bi 的限制。

const int N = 300 + 5;
const int mod = 998244353;

int T, n, m, f[N][N], s[N][N];
void add(int &x, int y) {
	x += y; if(x >= mod) x -= mod;
}
bool Inexist(int x1, int x2, int y1, int y2) {
	return s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] == 0;
}
int main() {
	cin >> T;
	while(T--) {
		cin >> n >> m, mem(f, 0, N), mem(s, 0, N);
		for(int i = 1, a, b; i <= m; i++)
			cin >> a >> b, s[a][b] += 1;
		for(int i = 1; i <= n; i++)
			for(int j = 1; j <= n; j++)
				s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
		bool oops = 0;
		for(int i = 1; i <= n; i++) {
			f[i][i] = 1;
			if(!Inexist(i, i, i, i)) oops = 1;
		} if(oops) {puts("0"); continue;}
		for(int len = 2; len <= n; len++)
			for(int l = 1, r = len; r <= n; l++, r++) {
				if(Inexist(l + 1, r, l, l)) add(f[l][r], f[l + 1][r]);
				if(Inexist(l, l, l + 1, r)) add(f[l][r], f[l + 1][r]);
				for(int k = l + 1; k < r; k++)
					if(Inexist(k + 1, r, l, k) && Inexist(l, l, l + 1, k))
						add(f[l][r], 1ll * f[l + 1][k] * f[k + 1][r] % mod);
			}
		cout << f[1][n] << endl;
	}
	return 0;
}

*XXX. P3607 [USACO17JAN]Subsequence Reversal

神仙区间 DP 题。

一个关键的 observatoin 是翻转一个子序列相当于交换若干对数,而 交换的数的位置之间只有包含关系,不交叉。这就为我们提供了区间 DP 的雏形。

同时注意到不降子序列是与值域有关的,所以值域也应放在 DP 状态中框起来。因此我们设计 fl,r,x,y 表示区间 [l,r] 值域在 [x,y] 之间的最长不降子序列长度,转移如下:

  • fl,r,x,yfl+1,r,x,y+[al=x]
  • fl,r,x,yfl,r1,x,y+[ar=y]
  • fl,r,x,yfl+1,r1,x,y+2×[al=yar=x]。这意味这交换两个 al,ar
  • fl,r,x,ymax(fl,r,x+1,y,fl,r,x,y1)

注意 DP 顺序。时间复杂度 O(n4)

为什么没有想到正解:没有注意到关键 observation 并想复杂了。其实很多区间 DP 比想象中简单,只是状态设计较为困难。

const int N = 50 + 5;

int n, a[N], f[N][N][N][N];
void cmax(int &x, int y) {if(y > x) x = y;}
int main() {
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> a[i];
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= a[i]; j++)
			for(int k = a[i]; k <= n; k++)
				f[i][i][j][k] = 1;
	for(int len = 2; len <= n; len++)
		for(int l = 1, r = len; r <= n; l++, r++) 
			for(int y = 1; y < N; y++)
				for(int x = y; x; x--) {
					f[l][r][x][y] = max(f[l][r][x + 1][y], f[l][r][x][y - 1]);
					cmax(f[l][r][x][y], f[l + 1][r][x][y] + (a[l] == x));
					cmax(f[l][r][x][y], f[l][r - 1][x][y] + (a[r] == y));
					cmax(f[l][r][x][y], f[l + 1][r - 1][x][y] + (a[l] == y) + (a[r] == x));
				}
	cout << f[1][n][1][N - 1] << endl;
	return 0;
}

*XXXI. P4766 [CERC2014]Outer space invaders

神仙区间 DP 题。

一个显然的 observation 是将时刻离散化,因为我们只关心外星人出现与发动攻击的相对顺序。

题解区 Cry_For_theMoon题解 总结的很棒:

区间 DP 的另一大套路就是:当我不好 "拆分-合并" 的时候,先考虑最后执行的操作,再用这个最后执行的操作把区间分成两部分,可以视作 "合并-拆分"。

因此,求 fl,r(表示消灭时刻完全包含于 [l,r] 的所有外星人的最小代价) 时,我们找到出现时间完全包含于当前区间且距离最远的外星人 i,那么我们肯定在 [ai,bi] 之间打了一次 di 的攻击。因此,枚举攻击点 k[ai,bi],只要 [l,k)(k,r] 的外星人都被消灭,[l,r] 的外星人就一定全部被消灭:跨过时刻 k 的外星人一定被 di 的攻击消灭了,因为我们选择的是 di 最大的 i

时间复杂度 O(n3)

为什么没有想到正解:没有接触过枚举最后一次操作后分裂区间的区间 DP。

const int N = 600 + 5;

int T, n, a[N], b[N], d[N], f[N][N];
int cnt, c[N];
int main() {
	cin >> T;
	while(T--) {
		cin >> n, cnt = 0, mem(f, 63, N);
		for(int i = 1; i <= n; i++)
			for(int j = 1; j < i; j++) f[i][j] = 0;
		for(int i = 1; i <= n; i++)
			cin >> a[i] >> b[i] >> d[i], c[++cnt] = a[i], c[++cnt] = b[i];
		sort(c + 1, c + cnt + 1), cnt = unique(c + 1, c + cnt + 1) - c - 1;
		for(int i = 1; i <= n; i++)
			a[i] = lower_bound(c + 1, c + cnt + 1, a[i]) - c,
			b[i] = lower_bound(c + 1, c + cnt + 1, b[i]) - c;
		for(int len = 1; len <= cnt; len++)
			for(int l = 1, r = len; r <= cnt; l++, r++) {
				int mxid = -1;
				for(int i = 1; i <= n; i++)
					if(l <= a[i] && b[i] <= r && (mxid == -1 || d[i] > d[mxid])) mxid = i;
				if(mxid == -1) {f[l][r] = 0; continue;}
				for(int i = a[mxid]; i <= b[mxid]; i++)
					f[l][r] = min(f[l][r], f[l][i - 1] + f[i + 1][r] + d[mxid]);
			}
		cout << f[1][cnt] << endl;
	}
	return 0;
}

*XXXII. P3592 [POI2015]MYJ

区间 DP 好题。和上面一题差不多的套路。

首先离散化 ci。设 fl,r,x 表示区间 [l,r] 最小值为 x 的答案,gl,r,x 表示区间 [l,r] 最小值不小于 x 的答案。由于要输出方案,所以还要记录 frl,r,x 表示 gl,r,x 是取的 fl,r,frl,r,x,以及 dl,r,x 表示 fl,r,x 的分割点。

那么转移就挺 trivial 的:枚举断点 k,则贡献为 gl,k1,x+gk+1,r,x+cx,其中 c 是满足 laikbiri 的个数,可以在枚举 l,r,k 的时候 O(m) 预处理。

时间复杂度 O(n3m)

const int N = 50 + 5;
const int M = 4000 + 5;

int n, m, cnt, a[M], b[M], c[M], d[M], res[N];
int f[N][N][M], mx[N][N][M], de[N][N][M];
void solve(int l, int r, int k) {
	if(l > r) return;
	if(l == r) return res[l] = d[k], void();
	int p = de[l][r][k]; res[p] = d[k];
	assert(p > 0);
	solve(l, p - 1, mx[l][p - 1][k]);
	solve(p + 1, r, mx[p + 1][r][k]);
}
int main() {
	cin >> n >> m;
	for(int i = 1; i <= m; i++)
		cin >> a[i] >> b[i] >> c[i], d[i] = c[i];
	sort(d + 1, d + m + 1), cnt = unique(d + 1, d + m + 1) - d - 1;
	for(int i = 1; i <= m; i++) c[i] = lower_bound(d + 1, d + cnt + 1, c[i]) - d;
	for(int len = 1; len <= n; len++)
		for(int l = 1, r = len; r <= n; l++, r++) {
			for(int k = l; k <= r; k++) {
				static int num[M]; mem(num, 0, cnt + 2);
				for(int p = 1; p <= m; p++)
					num[c[p]] += l <= a[p] && a[p] <= k && k <= b[p] && b[p] <= r;
				for(int p = cnt; p; p--) {
					int res = (num[p] += num[p + 1]) * d[p] + 
						f[l][k - 1][p] + f[k + 1][r][p];
					if(res >= f[l][r][p]) f[l][r][p] = res, de[l][r][p] = k;
				}
			}
			for(int i = cnt; i; i--) {
				if(f[l][r][i + 1] > f[l][r][i])
					f[l][r][i] = f[l][r][i + 1], mx[l][r][i] = mx[l][r][i + 1];
				else mx[l][r][i] = i;
			}
		}
	cout << f[1][n][1] << endl;
	solve(1, n, mx[1][n][1]);
	for(int i = 1; i <= n; i++) cout << res[i] << " ";
	cout << endl; 
	return 0;
}

XXXIII. P5774 [JSOI2016]病毒感染

题解

*XXXIV. BZOJ3971 [WF2013]Матрёшка

题解

*XXXV. P7163 [COCI2020-2021#2] Svjetlo

题解

*XXXVI. P6116 [JOI 2019 Final]家庭菜園

一道比较简单但细节的 DP。

实际上我们只关心当前位置摆放的花盆颜色,以及在此之前三种颜色的花盆分别有多少个。并且同种颜色花盆的相对位置不会改变。因此设 fi,j,k,p 表示使颜色为 0/1/2 的花盆个数分别为 i,j,k 且当前位置花盆颜色为 p 的最小代价(i+j+k 可以推出当前位置下标)。

转移直接枚举下一个位置的花盆颜色 p 即可,根据 i,j,k + 用桶记录每种颜色从左往右数第 x 个花盆的位置,可以轻松得到目标花盆的当前位置。注意已经进行的操作可能会让目标花盆的当前位置不等于初始位置(之前操作的目标花盆可能在当前目标花盆的初始位置之后),需要另行计算。

时空间复杂度 n3,有 16 的常数。

const int N = 400 + 5;
const int inf = 1e9;
int n, a, b, c, pos[3][N], f[3][135][205][N];
vint buc[3];
char s[N];
int main() {
	cin >> n >> s + 1;
	for(int i = 1; i <= n; i++)
		s[i] == 'R' ? buc[0].pb(i) : s[i] == 'G' ? buc[1].pb(i) : buc[2].pb(i);
	sort(buc, buc + 3, [](vint a, vint b) {return a.size() < b.size();});
	for(int i = 0; i < 3; i++) for(int it : buc[i]) pos[i][it]++;
	for(int i = 1; i <= n; i++) for(int j = 0; j < 3; j++) pos[j][i] += pos[j][i - 1]; 
	mem(f, 0x3f, 3), f[0][0][0][0] = 0;
	a = buc[0].size(), b = buc[1].size(), c = buc[2].size();
	for(int i = 0; i <= a; i++) for(int j = 0; j <= b; j++)
		for(int k = 0, np = i + j + 1; k <= c; k++, np++) for(int p = 0; p < 3; p++) {
			if(f[p][i][j][k] < inf) for(int q = 0; q < 3; q++)
				if(p != q || (!i && !j && !k)) {
					int val = f[p][i][j][k];
					if(q == 0 && i < a) {
						int Pos = buc[q][i];
						int Swp = max(0, j - pos[1][Pos]) + max(0, k - pos[2][Pos]);
						cmin(f[q][i + 1][j][k], val + max(0, Pos + Swp - np));
					} if(q == 1 && j < b) {
						int Pos = buc[q][j];
						int Swp = max(0, i - pos[0][Pos]) + max(0, k - pos[2][Pos]);
						cmin(f[q][i][j + 1][k], val + max(0, Pos + Swp - np));
					} if(q == 2 && k < c) {
						int Pos = buc[q][k];
						int Swp = max(0, i - pos[0][Pos]) + max(0, j - pos[1][Pos]);
						cmin(f[q][i][j][k + 1], val + max(0, Pos + Swp - np));
					}
				}
		}
	int ans = min(min(f[0][a][b][c], f[1][a][b][c]), f[2][a][b][c]);
	cout << (ans >= inf ? -1 : ans) << endl;
	return 0;
}

XXXVII. P7914 [CSP-S 2021] 括号序列

fi,j 表示区间 [i,j] 的答案,注意到 ASB 这种情况不太好处理,于是再设 gi,j 表示 AS 个数。再注意到 ()()() 这种情况会算重,那么设 bi,j 表示最外层是一层括号的答案,根据题意转移就完事了嗷。时间复杂度 O(n3)

const int N = 500 + 5;
const int mod = 1e9 + 7;
void add(int &x, int y) {x += y; if(x >= mod) x -= mod;}

int n, K, f[N][N], g[N][N], br[N][N], h[N][N]; // g : AS 
char s[N];
bool mt(int p, char t) {return s[p] == '?' || s[p] == t;}

bool Med;
int main() {
	cin >> n >> K >> s + 1;
	for(int i = 1; i <= n; i++)
		for(int j = i; j <= n; j++) {
			if(j - i + 1 > K) continue;
			h[i][j] = 1;
			for(int k = i; k <= j; k++) h[i][j] &= mt(k, '*');
		}
	for(int i = 1; i <= n + 1; i++) f[i][i - 1] = br[i][i - 1] = 1;
	for(int len = 2; len <= n; len++)
		for(int l = 1, r = len; r <= n; l++, r++) {
			int tmpf = 0, tmpbr = 0;
			for(int d = l; d < r; d++) {
				add(tmpf, 1ll * f[l][d] * br[d + 1][r] % mod); // case 2.1
				add(tmpf, 1ll * g[l][d] * br[d + 1][r] % mod); // case 2.2
				if(h[d + 1][r]) add(g[l][r], f[l][d]);
			}
			if(mt(l, '(') && mt(r, ')')) {
				add(tmpbr, h[l + 1][r - 1]); // case 1
				add(tmpbr, f[l + 1][r - 1]); // case 3.1
				for(int d = l + 1; d + 1 < r; d++) {
					if(h[l + 1][d]) add(tmpbr, f[d + 1][r - 1]); // case 3.2
					if(h[d + 1][r - 1]) add(tmpbr, f[l + 1][d]); // case 3.3
				}
			}
			f[l][r] = tmpf, br[l][r] = tmpbr;
			add(f[l][r], br[l][r]);
		}
	cout << f[1][n] << endl;
	return flush(), 0;
}

*XXXVIII. 2021 联考模拟北大附 图的直径

题意简述:两条 n+1 个点的链,点 i1i 之间的边权分别为 ai,bi。求添加不超过 k 条连接编号相同的点的边后图的直径最小值。1kn2001ai,bi50

形象地想一下,若 i 处有连边,则一段前缀 [0,i] 的合法性由上一个连边的位置 pi 之间的直径,前缀 [0,p] 的合法性与 [0,p]p 的最长距离与 [p,i]p 的最长距离相关。注意到状态实际与四个变量有关:前缀的位置 i,连边的条数 ji[0,i] 的最长距离 r 和直径 d,维度大小分别为 n,n,50n,50n。考虑把 i,j,r 作为维度,d 作为值域,有如下 DP 方程:fi,j,r 表示前缀 [0,i]j 条边且 i 处连边,[0,i]i 的最长距离为 r 时的最短直径。转移时需枚举上一个连边的位置 p,因此这样做的时间复杂度为 50n4,不可接受。

此时就需要挖掘一些性质了:首先,注意到转移是一个对决策取 min,贡献取 max 的过程。由于求直径的最小值是一路 max 过来的(决策的 min 仅保证了最优性而没有改变直径需要取 max 的既定事实),因此可以在最外层套一个二分将问题转化为存在性问题。这样复杂度变为 50n4logn,但 DP 的值域仅有 [0,1]

然后是一步非常关键的操作:根据单调性将值域与定义域对换。这里的单调性指的是若 fi,j,r=1fi,j,r+1 也一定为 1,这是显然的。因此我们可以把 r 这一维去掉!仅使用状态 fi,j 记录满足条件即 fi,j,r=1r 的最小值,通过直径 mid 的条件限制转移。这样就是 n3logn,因为 50nr 这一维直接消失了。

此外还需预处理一段区间 [l,r] 的直径(环上的 maxli,jrdis(i,j))与到端点 l,r 的最长距离,前者可以枚举 i 并用一个指针维护 j 因为有单调性,后者是 trivial 的。预处理复杂度为 n3。时间复杂度 O(n3logn)

Bonus:是否可以通过决策单调性优化做到 O(n2(n+log2n))?尝试实现后 WA 70,猜测没有决策单调性。

启发:遇到与最值有关的最优性 DP 时可以尝试先二分答案转化为判定性问题,然后一定单调性将定义域与值域互换从而优化复杂度。思路:设计朴素 DP(赛时这一步没做到) 运用最值转移与二分限制相转化的技巧变为判定性问题 发现单调性 定义域与值域互换。

#include <bits/stdc++.h>
using namespace std;
template <class T1, class T2> void cmin(T1 &a, T2 b){a = a < b ? a : b;}
template <class T1, class T2> void cmax(T1 &a, T2 b){a = a > b ? a : b;}
int n, m, k, a[233], b[233], da[233][233], db[233][233], dia[233][233], frl[233][233], frr[233][233];
bool check(int x) {
	static int f[233][233]; memset(f, 0x3f, sizeof(f)); for(int i = 0; i <= n; i++) f[i][1] = da[0][i] + db[0][i] <= x ? max(da[0][i], db[0][i]) : 1e9;
	for(int i = 1; i <= n; i++) for(int j = 2; j <= i + 1; j++) for(int k = 0; k < i; k++) if(dia[k][i] <= x && f[k][j - 1] + frl[k][i] <= x) cmin(f[i][j], max(frr[k][i], f[k][j - 1] + min(da[k][i], db[k][i])));
	for(int i = 0; i <= n; i++) if(f[i][k] + max(da[i][n], db[i][n]) <= x && da[i][n] + db[i][n] <= x) return 1; return 0;
}
int main(){
	cin >> n >> k; for(int i = 1; i <= n; i++) cin >> a[i], a[i] += a[i - 1]; for(int i = 1; i <= n; i++) cin >> b[i], b[i] += b[i - 1];
	for(int i = 0; i < n; i++) for(int j = i + 1; j <= n; j++) da[i][j] = a[j] - a[i]; for(int i = 0; i < n; i++) for(int j = i + 1; j <= n; j++) db[i][j] = b[j] - b[i];
	for(int i = 0; i < n; i++) for(int j = i + 1; j <= n; j++) for(int k = i; k <= j; k++) cmax(frl[i][j], max(min(da[i][k], da[k][j] + db[i][j]), min(db[i][k], db[k][j] + da[i][j])));
	for(int i = 0; i < n; i++) for(int j = i + 1; j <= n; j++) for(int k = i; k <= j; k++) cmax(frr[i][j], max(min(da[k][j], da[i][k] + db[i][j]), min(db[k][j], db[i][k] + da[i][j])));
	for(int i = 0; i < n; i++) for(int j = i + 1; j <= n; j++) {
		for(int k = i, p = i; k <= j; k++) {while(p < j && min(da[k][p], da[i][k] + da[p][j] + db[i][j]) < min(da[k][p + 1], da[i][k] + da[p + 1][j] + db[i][j])) p++; cmax(dia[i][j], min(da[k][p], da[i][k] + da[p][j] + db[i][j]));}
		for(int k = i, p = i; k <= j; k++) {while(p < j && min(db[k][p], db[i][k] + db[p][j] + da[i][j]) < min(db[k][p + 1], db[i][k] + db[p + 1][j] + da[i][j])) p++; cmax(dia[i][j], min(db[k][p], db[i][k] + db[p][j] + da[i][j]));}
		for(int k = i, p = j; k <= j; k++) {while(p > i && min(da[k][j] + db[p][j], da[i][k] + db[i][p]) < min(da[k][j] + db[p - 1][j], da[i][k] + db[i][p - 1])) p--; cmax(dia[i][j], min(da[k][j] + db[p][j], da[i][k] + db[i][p]));}
		for(int k = i, p = j; k <= j; k++) {while(p > i && min(db[k][j] + da[p][j], db[i][k] + da[i][p]) < min(db[k][j] + da[p - 1][j], db[i][k] + da[i][p - 1])) p--; cmax(dia[i][j], min(db[k][j] + da[p][j], db[i][k] + da[i][p]));}
	} int l = 0, r = 100 * n; while(l < r) {int m = l + r >> 1; check(m) ? r = m : l = m + 1;}
	return cout << l << endl, 0;
}

XXXIX. 2021 联考模拟巴蜀中学 叁仟柒佰万

题意简述:求分割一个序列使得每段 mex 值相等的方案数对 109+7 取模。

n3.7×107

看到题目,一个首先的想法是考察哪些值可能成为最终的 mex:从较为简单的情况开始入手。考虑若序列中不出现 0 则答案显然为 2n1,若出现 0 则必然至少有一段的 mex>0 推出每一段都有 mex>0,根据这个结论,若出现 1 则必然至少有一段 mex>1 推出每一段都有 mex>1 …… 类比数学归纳法不断递推下去直到我们到达整个序列的 mex。因此,本题的关键结论是 分割出的序列 mex 等于全局 mex

不妨设全局 mexd。假设我们固定了右端点 r,那么使得 mex(alar)=d 的合法 l 一定是一段前缀(或不存在),并且随着右端点 r 的右移,合法前缀的右端点也不断右移,这个不难证明。这给予我们一个 DP 的思路:设 fi 表示 a1ai 的答案,转移时直接枚举 l 使得 [l,i]mex 等于 d ,令 fifl1。根据上述结论维护合法前缀的右端点并使用前缀和优化即可。时间复杂度 O(n)

启示:从题目给出的限制反推出性质是很有用的方法。

const int N = 4e7 + 5;
const int mod = 1e9 + 7;

int a[N], buc[N], f[N];
void solve() {
	int n = read(), mex = 0, cur = 0;
	if(n == 37000000) {
		int x = read(), y = read(); a[1] = 0;
		for(int i = 2; i <= n; i++) a[i] = (1ll * a[i - 1] * x + y + i) & 262143;
	} else for(int i = 1; i <= n; i++) a[i] = read();
	for(int i = 0; i <= n + 4; i++) buc[i] = 0;
	for(int i = 1; i <= n; i++) buc[a[i]] = 1;
	while(buc[mex]) mex += 1;
	if(!mex) {
		int ans = 1, b = 2; n--;
		while(n) {
			if(n & 1) ans = 1ll * ans * b % mod;
			b = 1ll * b * b % mod, n >>= 1;
		} return cout << ans << endl, void();
	}
	for(int i = 0; i <= n + 4; i++) buc[i] = 0; f[0] = 1;
	for(int i = 1, pre = 1; i <= n; i++) {
		buc[a[i]]++;
		while(buc[cur]) cur++;
		if(cur == mex) {
			while(1) {
				if(a[pre] < mex && buc[a[pre]] == 1) break;
				buc[a[pre]]--, pre++;
			} f[i] = (f[pre - 1] + f[i - 1]) % mod;
		} else f[i] = f[i - 1];
	}
	cout << (f[n] - f[n - 1] + mod) % mod << endl;
}

*XL. CF1584F Strange LCS *2600

仍然是从简化版入手。考虑弱化版 “每个字符只出现一次” 怎么做:对于两个字符 x,y,如果它们在第一个字符中的出现位置 px,pypx>py,那么 x 不可能转移到 y。这给予我们 DP 的顺序:枚举从 1l1 的所有位置 i,j (1i<jl1),若 s1,i 在每个字符串的出现位置都在 s1,j 之前,那么 i 可以转移到 jcheckmax(fj,fi+1),其中 fi 表示 s1,1i 与其他所有字符串的 LCS 长度最大值。

不难将其扩展到本题的做法:只需要再记录一个 mask Sfi,S 表示匹配到每个字符串 s1,i 的前一个出现位置还是后一个出现位置。转移时枚举下一个字符 j 求出 最小的 mask(这里用了贪心的思想,能靠前尽量靠前显然更优)即可。时间复杂度 O(Tn\absΣ22n),代码写起来还是很舒服的。

#include <bits/stdc++.h>
using namespace std;

#define pii pair <int, int>
#define mem(x, v, s) memset(x, v, sizeof(x[0]) * (s)) 
const int N = 10;
const int S = 110;

int T, n, ap, aS, len[N], mp[1 << N];
int f[S][1 << N], buc[N][S], p[N][S][2];
pii tr[S][1 << N];
char s[N][S];
void print(int i, int S) {
	if(!i) return;
	print(tr[i][S].first, tr[i][S].second), putchar(s[0][i - 1]);
}
int main(){
	for(int i = 0; i < 26; i++) mp['a' + i] = i;
	for(int i = 0; i < 26; i++) mp['A' + i] = i + 26;
	cin >> T;
	while(T--) {
		cin >> n, mem(f, 0, S), mem(buc, 0, N), ap = aS = 0;
		for(int i = 0; i < n; i++) {
			cin >> s[i], len[i] = strlen(s[i]);
			for(int j = 0; j < len[i]; j++) {
				int id = mp[s[i][j]];
				p[i][id][buc[i][id]++] = j;
			}
		} for(int i = 0; i < len[0]; i++)
			for(int S = 0; S < 1 << n; S++) if(!i || f[i][S])
				for(int j = i + 1; j <= len[0]; j++) {
					int ok = 1, msk = 0, pr = i ? mp[s[0][i - 1]] : 0, su = mp[s[0][j - 1]];
					for(int q = 0; q < n && ok; q++) {
						int b = S >> q & 1;
						if(!buc[q][su] || i && buc[q][pr] <= b) {ok = 0; break;}
						if(!i) continue;
						int cp = p[q][pr][b], fd = -1;
						for(int k = 0; k < buc[q][su] && fd == -1; k++)
							if(p[q][su][k] > cp) fd = k;
						fd == -1 ? ok = 0 : msk |= fd << q;
					} int v = !i ? 1 : f[i][S] + 1;
					if(ok && f[j][msk] < v) {
						f[j][msk] = v, tr[j][msk] = {i, S};
						if(v > f[ap][aS]) ap = j, aS = msk;
					}
				} cout << f[ap][aS] << "\n", print(ap, aS), puts("");
	}
	return 0;
}

*XLI. AT3878 [ARC089D] ColoringBalls *3782

神题啊。第一步要想到将颜色相同的连续段缩成一个点,变成形如 BrbrbrBrbr...BrbrB 其中 B 是黑色球。注意每个连续段 rbrbr 最左边和最右边的两个红球,以及整个序列左右的 B 可以不出现。还有可能出现单独一个 r,这表示不需要 b 来染这一段。此外,对于一个红蓝连续段,开头两步必须是 rb,剩下来每一步都会新增一个 b 段且任何一种染色的颜色都可达到最终连续段的目标。由于后面使用的每次染色都必须在每个红蓝段第一个 b 之后,故我们贪心选择尽量前的 rb 操作分配给开头两步。

不妨设分配完之后被分配的第 ib(按操作序列顺序)之后有 si 个未被分配的操作,且其所对应的红蓝段有 cib(这意味着它还要 ci1 次操作),不难发现需满足 sij=ixci1,其中 x 是红蓝段的数量,故我们要让 ci 单调不增。这就给予我们 DP 的思路,但在此之前我们得确定一些东西:有多少个 rbr 要被分配掉,因为他们会影响 si。也就相当于需要枚举 x,y 分别表示红蓝段和全红段数量,然后提前分配 x+yr 和相对应的 xb

确定 si 之后设 fi,j,k 表示考虑到 [i,x] 的红蓝段,p=ixcp1=jci1=k 的方案数。首先枚举由哪个 c 转移过来。由于 c 值相同的红蓝段端本质相同,它们之间不区分顺序 所以还需枚举其个数 l,有转移:

fi,j,k=lc<k(xi+1l)fi+l,jkl,c (p[0,l],si+pjpl)

k 这一维前缀和优化,DP 复杂度 O(n3lnn)。另外考虑统计答案:对于 f1,j,k2x+2 个可空盒子(每段红蓝段左右的 r 以及序列左右的 B),剩下来 2j(多出来的 jrb )加上 x+y1(分割 x+y 个段的 B)加上 yyr 段)加上 xx 个红蓝段的 y)总共 2x+2y+2j1 个非空盒子,系数为 (n+2x14x+2y+2j)。最后乘上将红蓝段和红色段复合起来的方案系数 (x+yy) 即可。时间复杂度 O(n5lnn),常数大概是 132,可以通过。

启示:对于操作序列相关计数题,考虑哪些局面可以得到,并抛弃所有不必要的信息。如果一些不能确定的变量阻碍了思路,让问题看起来不可做,那就尝试枚举它

const int N = 70 + 5;
const int mod = 1e9 + 7;
void add(int &x, int y) {x += y, x >= mod && (x -= mod);}
void add(int &x, int y, int z) {x = y + z, x >= mod && (x -= mod);}
int n, k, ans, vis[N], sum[N];
int f[N][N][N], C[N << 1][N << 1];
char s[N];
bool init(int x, int y) {
	int cnt1 = 0, cnt2 = 0;
	for(int i = 0; i < k; i++) {
		if(s[i] == 'r') {
			if(cnt1 < x + y) cnt1++, vis[i] = 0;
			else vis[i] = 1;
		} else {
			if(cnt2 < x && cnt2 < cnt1) cnt2++, vis[i] = 0;
			else vis[i] = 1;
		}
	} if(cnt1 < x + y || cnt2 < x) return 0;
	for(int i = k - 1, p = x, num = 0; ~i; i--)
		if(vis[i]) num++;
		else if(s[i] == 'b') sum[p--] = num;
	return 1;
}
int main(){
	cin >> n >> k >> s, C[0][0] = 1;
	for(int i = 1; i < N << 1; i++)
		for(int j = 0; j <= i; j++)
			add(C[i][j], C[i - 1][j], j ? C[i - 1][j - 1] : 0);
	for(int x = 0; x <= k; x++)
		for(int y = 0; y <= k; y++)
			if(init(x, y)) {
				int lim = sum[1]; mem(f, 0, N);
				for(int k = 0; k <= lim; k++) f[x + 1][0][k] = 1;
				for(int i = x; i; i--) {
					for(int k = 0; k <= lim; k++) f[i][0][k] = 1;
					for(int j = 1; j <= sum[i] && (j + x + y << 1) - 1 <= n; j++)
						for(int k = 1; k <= lim; k++) {
							for(int l = 1; i + l <= x + 1 && k * l <= j && sum[i + l] >= j - k * l; l++)
								add(f[i][j][k], 1ll * f[i + l][j - k * l][k - 1] * C[x - i + 1][l] % mod);
							add(f[i][j][k], f[i][j][k - 1]);
						}
				} int res = 0;
				for(int j = 0; j <= lim; j++)
					add(res, 1ll * f[1][j][lim] * C[n + (x << 1) + 1][(x << 2) + (y + j << 1)] % mod);
				add(ans, 1ll * res * C[x + y][y] % mod);
			}
	return 0;                        
}

*XLII. CF1613D MEX Sequences

好题。乍一看没啥思路,考虑挖掘性质:

  • observation 1. 若出现 v,则之后不可能出现 v1

    不妨设 xp=v,显然 mexi=1pxi 等于 v+1v1。无论哪种情况在添加 v1mex 都一定 v+1,不符题意。

  • observation 2.xp1+2=xp,那么 xq (q>p) 只能为 xpxp2

    因为 xq 不能为 xp1,而 xp1 又没有出现过所以接下来的 mex 值只能为 xp1。得证。

  • observation 3. 用 v 表示连续若干个 v,符合题意的序列一定满足形如 0,1,,v2,(v,v2) 或者 1,其中括号是不需要出现的部分。

    • xp3xp+1 不可能出现。

    • 根据 observation 1 不可能出现 xp1=xp+1

    • xp2=xp+1,此时 mex 值一定为 xp1。那么前面必然是从 0 连续递增到 xp2 的一段,因为若前面这一段有下降的部分,那么只能是下降 2,但根据 observation 2 接下来 mex 不可能上升,不符合题意。

    • xp=xp+1xp+1=xp+1 是平凡的。

    • xp+2=xp+1 只能出现在 observation 2 所述的情况中,不会使 mex 增加。

    • xp+3xp+1 不可能出现。

    1 的情况平凡。证毕。

考虑设 gi 表示 1i 有多少以 ai 结尾的,单调不减且相邻两个数相差不超过 1 的子序列。转移时记录 fv 表示以 v 结尾的序列个数,那么若 ai=0gi=f0+1,若 ai>0gi=fai+fai1。不要忘记令 faifai+gi

接下来只需要考虑对于每个位置 ii+1n 有多少以 ai+2 开头且在 aiai+2 之间来回震荡的子序列。考虑从后往前 DP:设 cv 表示以 v 开头且在 vv2 之间来回震荡的子序列个数,dv 则表示 vv+2。这两个之间可以相互转移:

caicai+dai2+1daidai+cai+2+1

统计答案就比较简单了,首先正序处理出 gi,然后倒序计算 c,d,过程中令答案加上 gi×(cai+2+1) 即可。不要忘记最后 1,计算方案数是平凡的(实际上就是 c1)。时间复杂度线性。代码

XLIII. [BZOJ1402] Ticket to Ride

我们设 fi,S 表示点 i 连通所有关键点集 S 的最小代价,那么有:

fi,S+w(i,j)fj,Sfi,S+fi,Tfi,ST (ST=)

这不就是最小斯坦纳树么。后者按 S 从小到大 38 枚举子集转移,前者跑一遍最短路转移。由于并不是要全部连通,只要点对连通,所以最后还要爆搜 / 状压 DP 求答案。时间复杂度 O(n3k+nlogm2k+nk2),如果使用状压 DP 那么求答案的复杂度为 n3k2 而非 nk2k 是关键点个数,本题中为 8

const int N = 30 + 5;
int n, m, ans = 1e9, f[N][1 << 8], vis[N];
vpii e[N];
string s, t;
gp_hash_table <string, int> mp;
void Dijkstra(int S) {
	priority_queue <pii, vector <pii>, greater <pii>> q;
	for(int i = 1; i <= n; i++) if(f[i][S] < 1e9) q.push({f[i][S], i});
	mem(vis, 0, N);
	while(!q.empty()) {
		pii t = q.top(); q.pop();
		int id = t.se, ds = t.fi;
		if(vis[id]) continue;
		vis[id] = 1;
		for(pii it : e[id]) {
			int to = it.fi, d = ds + it.se;
			if(f[to][S] > d) q.push({f[to][S] = d, to});
		}
	}
}

void dfs(int tot, int S) {
	if(S == 15) return cmin(ans, tot), void();
	for(int T = (15 - S), msk = 0; T; T = (T - 1) & (15 - S), msk = 0) {
		for(int i = 0; i < 4; i++)
			if(T >> i & 1) msk |= (1 << i * 2) | (1 << i * 2 + 1);
		for(int i = 1; i <= n; i++) dfs(tot + f[i][msk], S | T);
	}
}
int main() {
	cin >> n >> m, mem(f, 0x3f, N);
	for(int i = 1; i <= n; i++) cin >> s, mp[s] = i;
	for(int i = 1, w; i <= m; i++)
		cin >> s >> t >> w, e[mp[s]].pb(mp[t], w), e[mp[t]].pb(mp[s], w);
	for(int i = 0; i < 4; i++)
		cin >> s >> t, f[mp[s]][1 << i * 2] = f[mp[t]][1 << i * 2 + 1] = 0;
	for(int S = 1; S < 1 << 8; S++) {
		for(int p = 1; p <= n; p++)
			for(int T = S; T; T = (T - 1) & S)
				cmin(f[p][S], f[p][T] + f[p][S - T]);
		Dijkstra(S);
	} dfs(0, 0), cout << ans << endl;
	return flush(), 0;
}
posted @   qAlex_Weiq  阅读(4012)  评论(3编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示