DP 做题记录

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

*I. P3643 [APIO2016]划艇

题意简述:给出序列 \(a_i,b_i\),求出有多少序列 \(c_i\) 满足 \(c_i=-1\)\(c_i\in[a_i,b_i]\),同时非 \(-1\) 的部分单调递增。

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

注意到值域过大,于是对区间进行离散化,设离散化后的端点分别为 \(p_1,p_2,\cdots,p_c\)。注意要将 \([a_i,b_i]\) 变成 \([a_i,b_i+1)\),即\(b_i\)\(1\),方便计算答案。

接下来考虑 DP:设 \(f_{i,j}\) 表示第 \(i\) 个学校派出划艇数量在 \(L_j=[p_j,p_{j+1})\) 之间时的方案数。

错误思路:\(f_{i,j}=\begin{cases}\sum_{pos=1}^{i-1}\sum_{k=1}^{j-1}f_{pos,k}\times (p_{j+1}-p_j)&[p_j,p_{j+1})\subseteq[a_i,b_i)\\0&\mathrm{otherwise}\end{cases}\)。错误原因:I. 没有考虑边界条件 & 枚举下界。II. 没有考虑在同一区间内也合法的情况。

边界条件就是 \(f_{i,0}=1\),并且注意 \(pos,k\) 的枚举下界应为 \(0\)

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

综上所述,更新后的转移方程应为 \(f_{i,j}=\begin{cases}\sum_{pos=0}^{i-1}\sum_{k=0}^{j-1}f_{pos,k}\times\binom{m+L_j-1}{m}&[p_j,p_{j+1})\subseteq[a_i,b_i)\\0&\mathrm{otherwise}\end{cases}\)。注意到后面的组合数可以 \(\mathcal{O}(1)\) 递推(\(\binom{m+L_j}{m+1}=\binom{m+L_j-1}{m}\times\frac{m+L_j}{m+1}\)),那么使用前缀和优化(因为 \(m\) 与枚举的 \(k\) 无关,所以记 \(s_{i,j}=\sum_{k=0}^j f_{i,k}\),则上面那一坨可以写成 \(\sum_{pos=0}^{i-1}s_{pos,j-1}\times\binom{m+L_j-1}{m}\))+ 倒序枚举 \(pos\)(实时更新 \(m\) 与组合数)即可 \(\mathcal{O}(n^3)\) 计算。

#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:设 \(f_{i,j}\) 表示前 \(i\) 个字母构成的有 \(j\) 对相邻字符的字符串个数。转移时枚举 \(j\),当前字母分几段,以及一共插入到几对相邻字符中,然后组合数算算即可。答案即为 \(f_{n,0}\)

时间复杂度 \(\alpha n^3\),其中 \(\alpha\) 是字符集大小,\(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

好题。首先考虑没有子集限制怎么做:单纯设一个状态好像不好转移,因此设 \(f_{l,r}\)​ 为不需要括号括起来的方案数,\(g_{l,r}\)​ 为只能用括号括起来或只有单字符的方案数,那么枚举第一个括号的结尾位置,有 \(f_{l,r}=\sum_{k=l}^r g_{l,k}f_{k+1,r}\)​,\(g_{l,r}=\sum_{d|r-l+1\ \land\ d<r-l+1}g_{l,l+d-1}[s_{l,l+d-1}=\cdots=s_{r-d+1,r}]\)注意观察边界条件​。

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

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

双倍经验,不过略有区别:设 \(f_{i,j}\)​ 表示中间没有 M 的方案数(假定 \(i\)​ 前面的所有字符不在缓冲串中),\(g_{i,j}\)​ 表示中间有 M 的方案数,那么有 \(f_{l,r}=\min_{k=l}^{r-1}(f_{i,k}+r-k)\)(为什么是 \(f_{i,k}\):因为我们假定进入 \([l,r]\) 时缓冲串为空,所以一定是前缀,如果是 \(f_{k,r}\)\(l\sim k-1\) 的字符就在缓冲串里面了),不要忘记用 \(r-l+1\) 以及 \(f_{l,mid}+1\)(如果 \(r-l+1\) 是偶数且 \(s_{l\sim mid}=s_{mid+1\sim r}\))更新 \(f_{l,r}\)\(g\) 就很简单了啊,枚举 M 出现的位置,那么,\(\min_{k=l}^{r-1}(\min(f_{i,k},g_{i,k})+\min(f_{k+1,r},g_{k+1,r})+1)\)​。​时间复杂度 \(\mathcal{O}(n^3)\)

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 操作究竟应该怎么用:设 \(B_j\)\(A_{1\sim i-1}\) 的最后一次出现在位置 \(x\)\(A_i\)\(B_{1\sim j-1}\) 的最后一次出现在位置 \(y\)​。​如果 \(x,y\) 至少有一个不存在,那么显然 swap 不优。否则我们删掉 \(A_{x+1\sim i-1}\),swap,再在中间插入 \(B_{y+1\sim j-1}\) 即可。

正确性其实挺难说明的,结合 \(2t_e\geq t_i+t_d\) 感性理解一下,时间复杂度 \(\mathcal{O}(n^2)\)

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\) 个集合,求每个集合中所有线段的并的长度之和的最大值。

\(k\leq n\leq 5000\)\(l\leq r\leq 10^6\)。时限 1s,空限 512MB。

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

因此,找到所有不完全覆盖任何其它线段的所有线段,按右端点排序,显然左端点也是单调的,所以设计 DP \(f_{i,j}\) 表示前 \(i\) 条前段划分成了 \(j\) 个集合的答案。最后 \(f_{n,j}\) 和完全覆盖的线段合并贡献即可。时间复杂度 \(\mathcal{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\) 个字符串 \(S_i\),求用这些字符串的所有子串拼出 \(T\) 的最小代价,与方案数 \(\bmod 998244353\)。使用一次 \(S_i\) 的子串代价为 \(C_i\),方案不同当且仅当 \(T\) 中存在至少一个字符满足来自不同的字符串。

\(|T|\leq 10^5\)\(n\leq 200\)\(C_i\leq 10^4\)\(|S_i|\leq 3\times 10^4\)。**保证 \(C_i\) 随机生成****。时限 6s。空限 512MB。

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

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 优化。优化完变成 \(2500\rm ms\) 了。

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;}

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

*XII. P2605 [ZJOI2010]基站选址

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

\[f_{i,j}=c_i+\min_{0\leq k<i} f_{k,j-1}+\left(\sum_{x=k+1}^i w_x[d_k,d_i\notin [d_x-s_x,d_x+s_x]]\right) \]

直接计算是 \(\mathcal{O}(n^3k)\) 的,无法接受。将目光聚焦在一轮 DP 中:我们维护对于每个 \(j<i\)\(f_j\)\(f_i\) 的贡献(除了与 \(j\) 无关的 \(c_i\))。一开始显然符合条件。考虑当 \(i\to i+1\) 时的变化:对于每个满足 \(d_{i-1}\leq d_x+s_x<d_i\)\(x\),它可能没有被覆盖到。为什么说是可能呢,因为它也可以被左边的基站覆盖。找到所有这样的 \(x\),那么所有 \(d_y<d_x-s_x\)\(y\)\(i\) 的贡献都要增加 \(c_x\),因为 \(x\) 不被覆盖。注意到 \(d\) 递增,故可以用小根堆维护 \(d_x+s_x\) 并每次取出 \(d_i\) 的元素,进行计算。显然 \(y\) 是一段前缀,那么就是区间修改,区间最值,线段树即可。

不要忘记最后再来一轮 \(f_{i,k}\) 的 DP,因为 \(n\) 不一定必须是基站。类似上面的转移,倒过来考虑并用大根堆维护 \(d_x-s_x\),每次取出 \(d_i<d_x-s_x\)\(x\) 加上贡献即可。时间复杂度 \(\mathcal{O}(nk\log n)\)

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。从高位往低位计算,设 \(f_{i,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]同类分布

枚举各位数字之和,设 \(f_{i,j,k,l}\) 表示考虑到第 \(i\) 位,各位数字之和为 \(j\),前 \(i\) 位模数字之和为 \(k\) 且是否达到上界。答案即为 \(f_{1,sum,0,l}\)

时间复杂度 \(\mathcal{O}(9^4\log^4 n)\),比较卡。

#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]手机号码

\(f_{i,j,k,l,m,n}\) 表示考虑到第 \(i\) 位,是否出现连续三个相邻数字,是否出现 \(4\)\(8\),是否顶到上界,当前连续段长 \(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 环节:考虑到度数很小,可以状压。设 \(f_{i,S}\) 表示不考虑 \(i\) 儿子集合 \(S\) 所获得的最大贡献。

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

  • 若不选 \((u,v)\),则 \(f_{x,S}=\sum_{son\notin S} f_{son,0}\)
  • 若选择 \((u,v)\),则贡献为 \(f_{u,0}+f_{v,0}\) 加上 \(u\)\(x\)\(v\)\(x\) 的路径上所有非端点 不考虑包含 \(u\)\(v\) 为子节点的儿子的贡献,记为 \(c\)。注意特判 \(u\)\(v=x\) 的情况。

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

时间复杂度 \(\mathcal{O}(m2^d+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\) 个士兵的贡献为 \(f_{i,j}\)。两种操作:更改所有 \(f_{i,*}\) 或给出 \((u,v)\),保证 \(v\)\(u\) 的 祖先(可能相等),求在 \(u\) 的子树中任意节点放置任意数量士兵,且在 \((u,v)\) 的简单路径上(不包括 \(u\),若 \(u,v\) 相等则包括 \(u\))最多一个节点放置任意数量士兵,同时士兵数量总和不超过 \(m\) 的最大贡献。

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

\(n\leq 2\times 10^4\)\(m\leq 50\)\(q\leq 2\times 10^3\)

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

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]战争调度

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

故时间复杂度为 \(\mathcal{O}(4^{n-1}(n-1))\)

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

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

\[f_{i,x+y+k}=\sum_{x=0}^{sz_i}\sum_{y=0}^{sz_j}\sum_{k=0}^{sz_i-x}f_{i,x}f_{j,y}A^{h_i-h_j}_{k}\dbinom{sz_i-x}{k} \]

根据树形背包的复杂度分析,枚举 \(x,y\) 的复杂度为 \(n^2\)。因此总时间复杂度为 \(\mathcal{O}(n^3)\)

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 即可。

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

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

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

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的键盘

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

\(w_{i,s}\) 是大于号,则有:

\[\forall k\in[x+y,sz_i+sz_j],\ f_{i,k}=\sum_{x=1}^{sz_i}\sum_{y=1}^{sz_j}f_{i,x}f_{j,y}\dbinom{k-1}{x}\dbinom{sz_i+sz_j-k}{sz_i-x} \]

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

树形背包再枚举一维,时间复杂度 \(\mathcal{O}(n^3)\)

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\) 的枚举范围:单次合并 \((sz_i+sz_j)sz_j\)\(\mathcal{O}(n^3)\) 的,但 \(sz_isz_j\) 就是 \(\mathcal{O}(n^2)\)。观察上面一题的 \(k\) 的枚举范围,发现都不超过 \(sz_j\),因此时间复杂度正确。

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。

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

  • \(j<p_i\)\(f_{i,j}=0\)
  • \(p_i\leq j<i\)\(f_{i,j}=f_{i-1,j}\),这表示位置 \(i\)\(1\)
  • \(j=i\),当 \(i\) 必须填 \(1\)\(f_{i,i}=0\),否则为 \(\sum_{\\k=p_i}^{i-1}f_{i-1,k}\)

注意到 \(p_i\) 是单调的,所以直接维护 \(s=\sum_{j=p_i}^{i-1}f_{i-1,j}\) 即可,时间复杂度 \(\mathcal{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 联考模拟知临 食堂计划

题意简述:给出一张带边权的图,求有多少条 \(S\to T\) 的最短路无序对 \((P_1,P_2)\) 满足不存在 \(i\neq S,T\) 使得 \(i\in P_1,P_2\)

\(n\leq 2\times 10^3\)\(m\leq 3\times 10^4\)

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

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

\[g_i={f_{s,i}\choose 2}-\sum_j g_jf_{j,i}^2 \]

其中 \(j\) 的拓扑序小于 \(i\)。注意由于不计入 \(P_1=P_2\) 的情况,所以若 \(S\to j\) 直接连边则 \(g_i\) 还需减去 \(\dbinom{f_{j,i}}{2}\),目的是为了减去 \(S\to j\to i\) 的无序路径对:由于 \(S\to j\) 是相同的,所以不同的无序对共有 \(\dbinom{f_{j,i}}2\) 个。

最后若 \(S\to T\) 直接连边则将答案再加上 \(1\) 即可。时间复杂度 \(\mathcal{O}(nm)\)

\(f_{i,j}\) 时不加判断 \(f_{i,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]中国象棋

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

  • 不摆象棋:\(f_{i,j,k}\to f_{i+1,j,k}\)
  • 在空列摆一个象棋:\(f_{i,j,k}\times (m-j-k)\to f_{i+1,j+1,k}\)
  • 在一个象棋的列摆一个象棋:\(f_{i,j,k}\times j\to f_{i+1,j-1,k}\)
  • 在空列摆两个象棋:\(f_{i,j,k}\times \dbinom{m-j-k}2\to f_{i+1,j+2,k}\)
  • 在空列和一个象棋的列各摆一个象棋:\(f_{i,j,k}\times (m-j-k)\times j\to f_{i+1,j,k+1}\)
  • 在一个象棋的列摆两个象棋:\(f_{i,j,k}\times \dbinom j 2\to f_{i+1,j-2,k+2}\)

时间复杂度 \(\mathcal{O}(n^3)\)。初始值 \(f_{0,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 的一维:设 \(f_{i,j}\) 表示从 \(i\) 开始能够合成 \(j\) 的右端点(若为 \(0\) 则不存在),类似倍增进行 DP 即可。时间复杂度 \(\mathcal{O}(n(V+\log n))\)

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。

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

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

那么设 \(g_{i,j,x,y}\) 表示区间 \([i,j]\) 未被发放的成绩最大值为 \(y\),最小值为 \(x\),那么原来的 \(f_{i,j}\) 可以用 \(\min_{1\leq x\leq y\leq n} g_{i,j,x,y}+a+b(y-x)\) 算出。有转移方程:

\[g_{l,r,x,y}=\min_{k=l}^{r-1}\min(g_{l,k,x,y}+g_{k+1,r,x,y},g_{l,k,x,y}+f_{k+1,r},f_{l,k}+g_{k+1,r,x,y}) \]

初始值 \(g_{i,i,x,y}=0\ (x\leq a_i\leq y)\)\(f_{i,i}=a\),时间复杂度 \(\mathcal{O}(n^5)\)

为什么没有想到解法:没有接触过值域设计在 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 方法:从大区间转移到小区间

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

时间复杂度 \(\mathcal{O}(n^2)\)

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\in [a,b]\)\(f\in [c,d]\) 使得 \(match_e<match_f\)。这个可以通过预处理二维前缀和做到。

\(f_{i,j}\) 表示第 \(l\sim r\) 个左括号都匹配完成且形成一段区间的方案数,有以下三种转移:

  • \(l\) 个左括号匹配的右括号放在区间 \([l+1,r]\) 右边。\(f_{l,r}\gets f_{l+1,r}\)。前提是 \([l,l]\)\([l+1,r]\) 没有限制。
  • \(l\) 个左括号匹配的右括号放在它右边,\(f_{l,r}\gets f_{l+1,r}\)。前提是 \([l+1,r]\)\([l,l]\) 没有限制。
  • \(l\) 个左括号匹配的右括号放在区间 \([l+1,k]\ (l<k<r)\) 右边,\(f_{l,r}\gets f_{l+1,k}\times f_{k+1,r}\)。前提是 \([l,l]\)\([l+1,k]\) 没有限制 且 \([k+1,r]\)\([l,k]\) 没有限制。

时间复杂度 \(\mathcal{O}(n^3)\),初始值 \(f_{i,i}=1\)。注意特判若 \(a_i=b_i\) 需要直接输出 \(0\)

为什么没有想到正解:不知道怎么处理 \(a_i,b_i\) 的限制。

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 状态中框起来。因此我们设计 \(f_{l,r,x,y}\) 表示区间 \([l,r]\) 值域在 \([x,y]\) 之间的最长不降子序列长度,转移如下:

  • \(f_{l,r,x,y}\gets f_{l+1,r,x,y}+[a_l=x]\)
  • \(f_{l,r,x,y}\gets f_{l,r-1,x,y}+[a_r=y]\)
  • \(f_{l,r,x,y}\gets f_{l+1,r-1,x,y}+2\times [a_l=y\land a_r=x]\)。这意味这交换两个 \(a_l,a_r\)
  • \(f_{l,r,x,y}\gets \max(f_{l,r,x+1,y},f_{l,r,x,y-1})\)

注意 DP 顺序。时间复杂度 \(\mathcal{O}(n^4)\)

为什么没有想到正解:没有注意到关键 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 的另一大套路就是:当我不好 "拆分-合并" 的时候,先考虑最后执行的操作,再用这个最后执行的操作把区间分成两部分,可以视作 "合并-拆分"。

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

时间复杂度 \(\mathcal{O}(n^3)\)

为什么没有想到正解:没有接触过枚举最后一次操作后分裂区间的区间 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 好题。和上面一题差不多的套路。

首先离散化 \(c_i\)。设 \(f_{l,r,x}\) 表示区间 \([l,r]\) 最小值为 \(x\) 的答案,\(g_{l,r,x}\) 表示区间 \([l,r]\) 最小值不小于 \(x\) 的答案。由于要输出方案,所以还要记录 \(fr_{l,r,x}\) 表示 \(g_{l,r,x}\) 是取的 \(f_{l,r,fr_{l,r,x}}\),以及 \(d_{l,r,x}\) 表示 \(f_{l,r,x}\) 的分割点。

那么转移就挺 trivial 的:枚举断点 \(k\),则贡献为 \(g_{l,k-1,x}+g_{k+1,r,x}+cx\),其中 \(c\) 是满足 \(l\leq a_i\leq k\leq b_i \leq r\)\(i\) 的个数,可以在枚举 \(l,r,k\) 的时候 \(\mathcal{O}(m)\) 预处理。

时间复杂度 \(\mathcal{O}(n^3m)\)

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。

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

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

时空间复杂度 \(n^3\),有 \(\dfrac 1 6\) 的常数。

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] 括号序列

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

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\) 个点的链,点 \(i-1\)\(i\) 之间的边权分别为 \(a_i,b_i\)。求添加不超过 \(k\) 条连接编号相同的点的边后图的直径最小值。\(1\leq k\leq n\leq 200\)\(1\leq a_i,b_i\leq 50\)

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

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

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

此外还需预处理一段区间 \([l,r]\) 的直径(环上的 \(\max_{l\leq i,j\leq r} \mathrm{dis}(i,j)\))与到端点 \(l,r\) 的最长距离,前者可以枚举 \(i\) 并用一个指针维护 \(j\) 因为有单调性,后者是 trivial 的。预处理复杂度为 \(n^3\)。时间复杂度 \(\mathcal{O}(n^3\log n)\)

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

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

#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 联考模拟巴蜀中学 叁仟柒佰万

题意简述:求分割一个序列使得每段 \(\rm mex\) 值相等的方案数对 \(10^9+7\) 取模。

\(n\leq 3.7\times 10^7\)

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

不妨设全局 \(\rm mex\)\(d\)。假设我们固定了右端点 \(r\),那么使得 \(\mathrm{mex}(a_l\sim a_r)=d\) 的合法 \(l\) 一定是一段前缀(或不存在),并且随着右端点 \(r\) 的右移,合法前缀的右端点也不断右移,这个不难证明。这给予我们一个 DP 的思路:设 \(f_i\) 表示 \(a_1\sim a_i\) 的答案,转移时直接枚举 \(l\) 使得 \([l,i]\)\(\rm mex\) 等于 \(d\) ,令 \(f_i\gets f_{l-1}\)。根据上述结论维护合法前缀的右端点并使用前缀和优化即可。时间复杂度 \(\mathcal{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\),如果它们在第一个字符中的出现位置 \(p_x,p_y\)\(p_x>p_y\),那么 \(x\) 不可能转移到 \(y\)。这给予我们 DP 的顺序:枚举从 \(1\)\(l_1\) 的所有位置 \(i,j\ (1\leq i< j\leq l_1)\),若 \(s_{1,i}\) 在每个字符串的出现位置都在 \(s_{1,j}\) 之前,那么 \(i\) 可以转移到 \(j\)\(\mathrm{checkmax}(f_j,f_i+1)\),其中 \(f_i\) 表示 \(s_{1,1\sim i}\) 与其他所有字符串的 LCS 长度最大值。

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

#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

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

不妨设分配完之后被分配的第 \(i\)\(\tt b\)(按操作序列顺序)之后有 \(s_i\) 个未被分配的操作,且其所对应的红蓝段有 \(c_i\)\(\tt b\)(这意味着它还要 \(c_i-1\) 次操作),不难发现需满足 \(s_i\geq \sum_{j=i}^x c_i-1\),其中 \(x\) 是红蓝段的数量,故我们要让 \(c_i\) 单调不增。这就给予我们 DP 的思路,但在此之前我们得确定一些东西:有多少个 \(\tt rb\)\(\tt r\) 要被分配掉,因为他们会影响 \(s_i\)。也就相当于需要枚举 \(x,y\) 分别表示红蓝段和全红段数量,然后提前分配 \(x+y\)\(\tt r\) 和相对应的 \(x\)\(\tt b\)

确定 \(s_i\) 之后设 \(f_{i,j,k}\) 表示考虑到 \([i,x]\) 的红蓝段,\(\sum_{p=i}^xc_p-1=j\)\(c_i-1=k\) 的方案数。首先枚举由哪个 \(c\) 转移过来。由于 \(c\) 值相同的红蓝段端本质相同,它们之间不区分顺序 所以还需枚举其个数 \(l\),有转移:

\[f_{i,j,k} =\sum_{l}\sum_{c<k}\dbinom{x-i+1}{l}f_{i+l,j-kl,c}\ (\forall p\in[0,l],s_{i+p}\geq j-pl) \]

\(k\) 这一维前缀和优化,DP 复杂度 \(\mathcal{O}(n^3\ln n)\)。另外考虑统计答案:对于 \(f_{1,j,k}\)\(2x+2\) 个可空盒子(每段红蓝段左右的 \(\tt r\) 以及序列左右的 \(\tt B\)),剩下来 \(2j\)(多出来的 \(j\)\(\tt rb\) )加上 \(x+y-1\)(分割 \(x+y\) 个段的 \(\tt B\))加上 \(y\)\(y\)\(\tt r\) 段)加上 \(x\)\(x\) 个红蓝段的 \(\tt y\))总共 \(2x+2y+2j-1\) 个非空盒子,系数为 \(\dbinom{n+2x-1}{4x+2y+2j}\)。最后乘上将红蓝段和红色段复合起来的方案系数 \(\dbinom{x+y}y\) 即可。时间复杂度 \(\mathcal{O}(n^5\ln n)\),常数大概是 \(\dfrac 1{32}\),可以通过。

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

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\),则之后不可能出现 \(v-1\)

    不妨设 \(x_p=v\),显然 \(\mathrm{mex}_{i=1}^px_i\) 等于 \(v+1\)\(v-1\)。无论哪种情况在添加 \(v-1\)\(\mathrm{mex}\) 都一定 \(\geq v+1\),不符题意。

  • observation 2.\(x_{p-1}+2=x_p\),那么 \(x_q\ (q>p)\) 只能为 \(x_p\)\(x_p-2\)

    因为 \(x_q\) 不能为 \(x_p-1\),而 \(x_{p}-1\) 又没有出现过所以接下来的 \(\mathrm{mex}\) 值只能为 \(x_{p}-1\)。得证。

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

    • \(x_{p}-3\geq x_{p+1}\) 不可能出现。

    • 根据 observation 1 不可能出现 \(x_p-1=x_{p+1}\)

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

    • \(x_p=x_{p+1}\)\(x_p+1=x_{p+1}\) 是平凡的。

    • \(x_p+2=x_{p+1}\) 只能出现在 observation 2 所述的情况中,不会使 \(\mathrm{mex}\) 增加。

    • \(x_p+3\leq x_{p+1}\) 不可能出现。

    \(\overline{1}\) 的情况平凡。证毕。

考虑设 \(g_i\) 表示 \(1\sim i\) 有多少以 \(a_i\) 结尾的,单调不减且相邻两个数相差不超过 \(1\) 的子序列。转移时记录 \(f_v\) 表示以 \(v\) 结尾的序列个数,那么若 \(a_i=0\)\(g_i=f_0+1\),若 \(a_i>0\)\(g_i=f_{a_i}+f_{a_i-1}\)。不要忘记令 \(f_{a_i}\gets f_{a_i}+g_i\)

接下来只需要考虑对于每个位置 \(i\)\(i+1\sim n\) 有多少以 \(a_i+2\) 开头且在 \(a_i\)\(a_i+2\) 之间来回震荡的子序列。考虑从后往前 DP:设 \(c_v\) 表示以 \(v\) 开头且在 \(v\)\(v-2\) 之间来回震荡的子序列个数,\(d_v\) 则表示 \(v\)\(v+2\)。这两个之间可以相互转移:

\[c_{a_i}\gets c_{a_i}+d_{a_i-2}+1\\ d_{a_i}\gets d_{a_i}+c_{a_i+2}+1 \]

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

XLIII. [BZOJ1402] Ticket to Ride

我们设 \(f_{i,S}\) 表示点 \(i\) 连通所有关键点集 \(S\) 的最小代价,那么有:

\[f_{i,S}+w(i,j)\to f_{j,S}\\ f_{i,S}+f_{i,T}\to f_{i,S\cup T}\ (S\cap T=\varnothing) \]

这不就是最小斯坦纳树么。后者按 \(S\) 从小到大 \(3^8\) 枚举子集转移,前者跑一遍最短路转移。由于并不是要全部连通,只要点对连通,所以最后还要爆搜 / 状压 DP 求答案。时间复杂度 \(\mathcal{O}(n3^k+n\log m2^k+n^\frac{k}2)\),如果使用状压 DP 那么求答案的复杂度为 \(n3^{\frac k 2}\) 而非 \(n^{\frac k 2}\)\(k\) 是关键点个数,本题中为 \(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 @ 2021-12-07 18:10  qAlex_Weiq  阅读(2820)  评论(2编辑  收藏  举报