洛谷 普及/提高- DP题总结

目录

P1681 最大正方形II

P3183 [HAOI2016]食物链

P2904 [USACO08MAR]River Crossing S

P1470 [USACO2.3]最长前缀 Longest Prefix

P1922 女仆咖啡厅桌游吧

P2004 领地选择

P2327 [SCOI2005]扫雷

P2946 [USACO09MAR]Cow Frisbee Team S

P2918 [USACO08NOV]Buying Hay S

P5414 [YNOI2019] 排序

P1233 木棍加工

P2690 [USACO04NOV]Apple Catching G

P2782 友好城市

P1481 魔族密码

P1020 [NOIP1999 普及组] 导弹拦截

P1057 [NOIP2008 普及组] 传球游戏

P1455 搭配购买

P1466 [USACO2.2]集合 Subset Sums

P1564 膜拜


P1681 最大正方形II

        最大正方形要求以0/1相间的格子组成正方形,是“最大正方形”的变形,可以通过二维数组三组取min来求解。f[i][j][0]代表该位置是0,f[i][j][1]代表该位置是1。

	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(maps[i][j]==0){
				f[i][j][0]=min(f[i][j-1][1],min(f[i-1][j][1],f[i-1][j-1][0]))+1;
			}
			else{
				f[i][j][1]=min(f[i][j-1][0],min(f[i-1][j][0],f[i-1][j-1][1]))+1;
			}
			ans=max(ans,max(f[i][j][0],f[i][j][1]));
		}
	}

P3183 [HAOI2016]食物链

        同P4017最大食物链计数,可以用拓扑排序或是记忆化搜索来完成。

inline void tuopu(){
	while(!q.empty()){
		int t=q.front();
		q.pop();
		for(int i=head[t];i;i=e[i].next){
			int v=e[i].to;
			if(e[i].val==-1)continue;
			e[i].val=-1;
			sum[v]+=sum[t];
			rec[v]--;
			if(rec[v]==0){
				if(to[v]==0){
					ans+=sum[v];
				}
				q.push(v);
			}
		}
	}
}

P2904 [USACO08MAR]River Crossing S

        前缀和考察知识,同时巧妙转化了载奶牛过河的时间(将每次分批时间保存在前缀和数组中),并把路程来回的消耗时间算在其中,最后加上。值得细分析。

//dp[j]表示载j头奶牛所花最短时间,sum[i]表示一次载i头奶牛的时间 
	for(int i=1,t;i<=n;i++){
		dp[i]=inf;
		scanf("%d",&t);
		sum[i]=sum[i-1]+t;
	}
	
    for(int i=1;i<=n;i++)
		sum[i]+=2*m;
	
	for(int i=1;i<=n;i++){
		for(int j=i;j<=n;j++){
			dp[j]=min(dp[j],dp[j-i]+sum[i]);
		}
	}
	printf("%d",dp[n]-m);//最后一次不用返回,所以ans-m

P1470 [USACO2.3]最长前缀 Longest Prefix

        字符串dp,需要认真看。字符串的dp大部分都是匹配问题。要把字符串的匹配练好。核心代码还是for循环的匹配和对位,加上一些可以的小剪枝,没有什么思维技巧,就是for循环枚举。

    while(1)//输入单词 
	{
		n++;
		scanf("%s",a[n].ss+1);a[n].len=strlen(a[n].ss+1);
		if(a[n].len==1 && a[n].ss[1]=='.')//遇到“.”就退出 
		    { n--;break; }
		
	}
	while(scanf("%s",s+1)!=EOF)//输入字符串 
	{
		for(i=1;i<=strlen(s+1);i++){len++;st[len]=s[i];}
	}
	memset(f,false,sizeof(f));f[0]=true;//f[i]表示第i位是否能到达,初始化0是可以到达的 
	for(i=1;i<=len;i++)//枚举长度 
	{
		for(j=1;j<=n;j++)//枚举单词 
		{
			if(i=i-a[j].len+1;k--,t--)
			{
				if(st[k]!=a[j].ss[t])//如果不匹配 
					bk=false;break;
			}
			if(bk==true)//如果能成功的匹配了 
				f[i]=true;break;
		}
	}

P1922 女仆咖啡厅桌游吧

        树形dp的基础题,有一定思维考察性。要求每棵树cafe==table,则具体怎么放不会对最终的ans有影响,只要把叶子结点(通过记录入度来判断)的个数记录下来对半分,再累加上子树的sum就可得到最终答案。

inline void dfs(int u,int fa){
	int sum=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(v==fa)continue;
		dfs(v,u);
		if(in[v]==1)sum++;
		else{
			dp[u]+=dp[v];
		}
	}
	dp[u]+=sum/2;
}

P2004 领地选择

        二维前缀和求正方形最大面积。板子。

sum[i][j]=sum[i-1][j]+sum[i][j-1]+maps[i][j]-sum[i-1][j-1];
//二维前缀和

if(sum[i][j]+sum[i-c][j-c]-sum[i][j-c]-sum[i-c][j]>ans[2]){
	ans[0]=i-c+1,ans[1]=j-c+1,ans[2]=sum[i][j]+sum[i-c][j-c]-sum[i][j-c]-sum[i-c][j];
}//更新ans

P2327 [SCOI2005]扫雷

        巧妙设计状态。对于题中的限制条件,在一个2列的地图中只会存在最大为‘3’的地雷。当然包括0/1/2,所以我们可以通过显示的数字来推倒方案情况。针对本题用四维数组存储,因为最大地雷数是‘3’,即对于任意一格来说,他能推算的状况仅限于前一个格子、当前格子和后一个格子。我们前一维表示当前位置i,后三维就用来表示三个位置有地雷的情况。

    f[0][0][0][0]=f[0][0][0][1]=1;
	for(int i=1;i<=n;i++){
		if(!a[i])f[i][0][0][0]=f[i-1][0][0][0]+f[i-1][1][0][0];
		if(a[i]==1){
			f[i][1][0][0]=f[i-1][0][1][0]+f[i-1][1][1][0];
			f[i][0][1][0]=f[i-1][0][0][1]+f[i-1][1][0][1];
			f[i][0][0][1]=f[i-1][0][0][0]+f[i-1][1][0][0];
		}
		if(a[i]==2){
			f[i][1][1][0]=f[i-1][0][1][1]+f[i-1][1][1][1];
			f[i][0][1][1]=f[i-1][0][0][1]+f[i-1][1][0][1];
			f[i][1][0][1]=f[i-1][1][1][0]+f[i-1][0][1][0];
		}
		if(a[i]==3){
			f[i][1][1][1]=f[i-1][0][1][1]+f[i-1][1][1][1];
		}
	}

P2946 [USACO09MAR]Cow Frisbee Team S

        USACO的题目不会有太长的码量,但考察对于基本知识点的灵活运用的理解,我们要耐心分析转移过程,可得

状态转移方程如下:

f[i][j] = ( f[i][j] + f[i-1][j] ) % mod + f[i-1][j - cow[i] + f)%f ] )%mod

即分奶牛参与或不参与两种情况进行讨论。

	for(int i=1;i<=n;i++){
		scanf("%d",&r[i]);
		r[i]%=f;dp[i][r[i]]=1;//输入时%f来减小运算范围,因此下面会有+f)%f操作
	}
	for(int i=1;i<=n;i++)
		for(int j=0;j

P2918 [USACO08NOV]Buying Hay S

        完全背包,活用性质求最小。

    for(int i=1;i<=m+5000;i++)
    dp[i]=1e9;//初始化,因为要找到一个最小值,dp[i]表示得到i磅的干草最少需要的钱数 
    for(int i=1;i<=n;i++)
    scanf("%d%d",&a[i],&b[i]);
    for(int i=1;i<=n;i++)
        for(int j=a[i];j<=m+5000;j++)
    //注意循环结束为m+5000,因为你只购买m千克时花费的钱不一定是最少的,5000时一坨草质量的最大值 
        dp[j]=min(dp[j],dp[j-a[i]]+b[i]);
    for(int i=m;i<=m+5000;i++)
        ans=min(ans,dp[i]);//寻找哪一个既符合购买量,钱又最少 

P5414 [YNOI2019] 排序

        本题要求我们移动几个数字使得序列单增。明确一点:最优解不会把同一个数字移动两次及以上,要一步到位。因此问题转变,当我们在原数列中删除几个数(他们的和最小)让剩下的数单调递增就好。

		//f[i]记录前i位单调上升的数列之和的最大值
        for(int i=1;i<=n;i++){
			for(int j=1;j

P1233 木棍加工

        求最长下降子序列的变形,建立struct来排序后遍历讨论。

bool cmp(stick a,stick b){
	if(a.l==b.l)return a.w>b.w;
	return a.l>b.l;
}
    
    sort(a+1,a+1+n,cmp);
	for(int i=1;i<=n;i++){
		for(int j=i-1;j>0;j--){
			if(a[i].w>a[j].w){
				dp[i]=max(dp[i],dp[j]+1);
			}
			ans=max(ans,dp[i]);
		}
	}

P2690 [USACO04NOV]Apple Catching G

        Bessie的移动会有时间和移动次数的双重限制,用dp[i][j]表示奶牛在第i分钟内转移了j次能接到的最大苹果数。然后巧妙判断Bessie是否能得到苹果:a[i]==j%2+1

	for(int i=1;i<=t;i++){
		for(int j=0;j<=min(w,t);j++){
			if(j==0) dp[i][j]=dp[i-1][j];
			else dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]);
			if(a[i]==j%2+1) dp[i][j]++;
		}
	}

P2782 友好城市

        尽可能减少航线交叉,那就是要求解最长的单调队列长度。先sort对于结构体排序,之后为了应对庞大的数据量而采用二分查找(nlogn)降低复杂度。关于二分查找可以用lower_bound()(“>=”)和upper_bound()(“>”)来简单实现,这两个函数很有实用性,配合指针可以快速查询和更改。

inline bool cmp(node a,node b){
	return a.northd[len]){
			d[++len]=tmp;
		}
		else{
			*upper_bound(d+1,d+1+len,tmp)=tmp;//找最长上升序列用lower_bound,最长不下降用upper_bound
            // *  -->指针查询该位置上的值
		}
	}

P1481 魔族密码

        关于STL的几个骚函数操作:

        (1)strstr(s1,s2)
                作用:
                判断 s2 是否为 s1 的 子串。
                如果没找到,返回NULL;
                如果找到了,返回这个子串第一个字符的 地址。

                如:
                char s1[]="habch",s2[]="abc"
                strstr(s1,s2)就返回s1中'a'的地址

                头文件为:
                #include

        (2) # inlcudefind( )函数:转载链接,更好的find体验

        (3) 一种构造string的方法 : s.substr(pos, len)

        返回值: string,包含s中从pos开始的len个字符的拷贝(pos的默认值是0,len默认是s.size()         - pos,即不加参数会默认拷贝整个s)

        这道题我们可以偷懒将stl的函数巧用来取代手写for循环匹配。

    ios::sync_with_stdio(0);//cin,cout优化
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>w[i].s;
		f[i]=1;
		for(int j=i-1;j>=1;j--){
			if(strstr(w[i].s,w[j].s)==w[i].s){
				f[i]=max(f[i],f[j]+1);
			}
		}
		ans=max(ans,f[i]);
	}

P1020 [NOIP1999 普及组] 导弹拦截

        第一个问题求出最长不上升子序列的长度,第二问要求出最长上升序列长度。关于第二问的证明,可详见洛谷题解。

(1)假设打导弹的方法是这样的:取任意一个导弹,从这个导弹开始将能打的导弹全部打完。而这些导弹全部记为为同一组,再在没打下来的导弹中任选一个重复上述步骤,直到打完所有导弹。

(2)假设我们得到了最小划分的K组导弹,从第a(1<=a<=K)组导弹中任取一个导弹,必定可以从a+1组中找到一个导弹的高度比这个导弹高(因为假如找不到,那么它就是比a+1组中任意一个导更高,在打第a组时应该会把a+1组所有导弹一起打下而不是另归为第a+1组),同样从a+1组到a+2组也是如此。那么就可以从前往后在每一组导弹中找一个更高的连起来,连成一条上升子序列,其长度即为K;

(3)设最长上升子序列长度为P,则有K<=P;又因为最长上升子序列中任意两个不在同一组内(否则不满足单调不升),则有P>=K,所以K=P。

                                                                                                                 ——来源于洛谷题解

while(scanf("%d",&h[++n])!=EOF);n--;
	d[++len1]=h[1];
	t[++len2]=h[1];
	for(int i=2;i<=n;i++){
		int tmp=h[i];
		if(tmp<=d[len1]){
			d[++len1]=tmp;
		}
		else{
			*upper_bound(d+1,d+1+len1,tmp, greater<int>())=tmp;
        //greater使得能在一个单调递减的数列中查询大小关系
        //greater --> 
		}
		if(tmp>t[len2]){
			t[++len2]=tmp;
		}
		else{
			*lower_bound(t+1,t+1+len2,tmp)=tmp;
		}
	}

P1057 [NOIP2008 普及组] 传球游戏

        当前在传球了i次后,在第j个人手中的情况数,等于第i-1次传球后,球在第j+1和j-1号人手中的方案数总和。(看了题解挺好想)

    f[0][1]=1;
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			if(j==1){    //注意特判
				f[i][j]+=f[i-1][j+1]+f[i-1][n];
			}
			else{
				if(j==n){//注意特判
					f[i][j]+=f[i-1][j-1]+f[i-1][1];
				}
				else
					f[i][j]+=f[i-1][j-1]+f[i-1][j+1];
			}
		}
	}
	printf("%d",f[m][1]);

P1455 搭配购买

        缩点或是并查集来减少物品数量,之后就是0/1背包。

tarjan:

//tarjan
inline void tarjan(int u){
	dfn[u]=low[u]=++idx;
	S.push(u); 
	vis[u]=1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(vis[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		int t=-1;
		tot++;
		while(t!=u){
			t=S.top();
			belong[t]=tot;
			S.pop();
			vis[t]=0;
			Cost[tot]+=cost[t];
			Val[tot]+=val[t];
		}
	}
}
//0/1 backage
    for(int i=1;i<=tot;i++){
		for(int j=v;j>=Cost[i];j--){
			f[j]=max(f[j],f[j-Cost[i]]+Val[i]);
		}
	}

并查集:

void merge(int x,int y) {//并查集合并
	int fx=getfather(x);
	int fy=getfather(y);
	if(fx!=fy) {
		v[fx]+=v[fy];//合并成本
		p[fx]+=p[fy];//合并价格
		father[fy]=fx;
	}
}

P1466 [USACO2.2]集合 Subset Sums

        转化为0/1背包问题,我们要找到1~n中的几个数之和是1~n之和的1/2,先判断能否和整除2,在将sum/2作为容积来求解方案数。f[i][j]表示前i个数填满容积为j的背包的方案数。

	int sum=n*(n+1)/2;
	if(sum%2!=0){
		printf("0");return 0;
	}
	f[1][1]=1;
	for(int i=2;i<=n;i++){
		for(int j=0;j<=sum;j++){
			if(j>i)f[i][j]=f[i-1][j]+f[i-1][j-i];
			else f[i][j]=f[i-1][j];
		}
	}
	printf("%d",f[n][sum/2]);

P1564 膜拜

        dp[i]表示前i个人所需的最少机房数。前缀和+差分思想。

        如果第i个人是1,那么sum[i]=sum[i-1]+1, 否则sum[i]=sum[i-1]-1
        if(abs(sum[i]-sum[j-1]==i-j+1)),说明i到j都相同
        if(abs(sum[i]-sum[j-1]<=m)),说明i和j之间的不同人数差不超过m 
        在符合以上两个条件下,转移方程:dp[i]=min(dp[i],dp[j-1]+1) 

    for(int i=1;i<=n;i++)
    {
        read(be[i]);
        if(be[i]==1) sum[i]=sum[i-1]+1;        //前缀和 
        else sum[i]=sum[i-1]-1;
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            if(abs(sum[i]-sum[j-1])==i-j+1||abs(sum[i]-sum[j-1])<=m)
                dp[i]=min(dp[i],dp[j-1]+1);

你可能感兴趣的:(动态规划)