再读《挑战程序设计竞赛》——初出茅庐(3)

记录结果再利用的“动态规划”

动态规划重点一是状态表示和集合的划分(思维的难点),二是边界问题的处理,三是巧妙利用递推式的形式,考虑计算的顺序或者数据结构进行优化(代码实现中的难点)。
状态表示需要题感,自己去试试,集合划分则需要寻找第一个不同的状态进行划分。

书上的问题:
01背包问题
超超有名的背包问题,白书上的循序渐进的方法可以让我们对记忆化搜索和dp的理解更深刻。dp到底是怎么打表的?
书上先谈了一个朴素的暴力做法:

int dfs(int i,int j)
{
	if(i==n)//已经没有剩余物品了
		res=0;
	else if(j<w[i])//无法挑选这个物品 
		res=dfs(i+1,j);
	else//可以挑选也可以不挑选,选最大值 
		res=max(dfs(i+1,j),dfs(i+1,j-w[i])+v[i]);
	return res;
}

由于递归中,很多重复使用的dfs状态,我们用一个dp数组来保存它,就得到了以下记忆化搜索的方式:

int dfs(int i,int j)
{
	if(dp[i][j])
		return dp[i][j];
	if(i==n)//已经没有剩余物品了
		res=0;
	else if(j<w[i])//无法挑选这个物品 
		res=dfs(i+1,j);
	else//可以挑选也可以不挑选,选最大值 
		res=max(dfs(i+1,j),dfs(i+1,j-v[i])+w[i]);
	return dp[i][j]=res;
}

注意,如果把价值之和sum写在参数上,不记录在res中,反而会难以记忆化搜索。
用dp可以写成:

for(int i=1;i<=n;i++)
{
	for(int j=0;j<=W;j++)
	{
		if(j<w[i])
		{
			f[i][j]=f[i-1][j];
		}
		else
		{
			f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i];
		}
	}
} 

dp问题的滚动数组再利用

我们发现i都是由i-1推来的,所以我们用一维数组就可以写01背包啦。
而j为什么要反向推呢?因为反向推的时候,dp数组更新是从大数更到小数的,这样大数变成i时候的值,小数还是i-1时候的值,不会影响到递推。

for(int i=1;i<=n;i++)
{
	for(int j=W;j>=w[i];j--)
	{
		f[j]=max(f[j],f[j-w[i]]+v[i]);
	}
} 

根据背包九讲
我们还有如下问题:(摘自acwing的提高课)

  1. 完全背包问题
  2. 混合背包问题
  3. 多重背包问题
  4. 二维费用背包问题
  5. 分组背包问题
  6. 有依赖的背包问题
  7. 背包问题求方案数
  8. 背包问题求具体方案

限于篇幅,在这里只提一下完全背包和多重背包。
完全背包问题
如果我们递推只需要用到这一层和上一层的状态,我们可以用滚动数组优化,隔两层就会覆盖掉,节省了空间。一般采用f[i&1][j]的方式存储,在书上p60页的例子,我们用滚动数组优化完全背包问题:

void solve()
{
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<m;j++)
		{
			for(int k=0;k*v[i]<=j;k++)
			{
				dp[i][j]=max(dp[i][j],dp[i][j-k*v[i]]+k*w[i]);
			}
		}
	}
}

朴素的算法是O(nm2)的,显然不够优秀。
dp[i][j]=max(dp[i][j],dp[i][j-v]+w,…dp[i][j-k*v]+kw…)
dp[i][j-v]=max(dp[i][j-v],…,dp[i][j-kv]+kw…)
因此
dp[i][j]=max(dp[i][j],dp[i][j-v])
如果我们把它优化成一维,需要正向递推体积,因为这里更新dp[i][j]的是dp[i][j-v],要先覆盖掉i-1个物品的选取信息再更新。

#include<iostream>
using namespace std;
const int N=100010;
int v[N],w[N],a[N];
int main()
{
    int n,V;
    cin>>n>>V;
    for(int i=0;i<n;i++)
    {
        cin>>v[i]>>w[i];
    }
    for(int i=0;i<n;i++)
        for(int j=v[i];j<=V;j++)
        {
            a[j]=max(a[j],a[j-v[i]]+w[i]);
        }
    cout<<a[V];
    return 0;
}

多重背包问题

#include<iostream>
using namespace std;
const int N=1010;
int f[N];
int main()
{
    int n,V;
    cin>>n>>V;
    for(int i=0;i<n;i++)
    {
        int v,w,s;
        cin>>v>>w>>s;
        for(int j=V;j>=v;j--)
        {
            for(int k=1;k*v<=j&&k<=s;k++)
            {
                f[j]=max(f[j],f[j-v*k]+w*k);
            }
        }
    }
    cout<<f[V];
    return 0;
}

多重背包问题有两种优化方式
103数量级,用二进制优化

#include<bits/stdc++.h>
using namespace std;
const int N=25000;
int v[N],w[N],f[N];
int cnt=0;
int main()
{
    int N,V;
    cin>>N>>V;
    for(int i=0;i<N;i++)
    {
        int a,b,s;
        cin>>a>>b>>s;
        
        int k=1;
        
        while(k<=s)
        {
            cnt++;
            v[cnt]=k*a;
            w[cnt]=k*b;
            s-=k;
            k*=2;
            
        }
        if(s>0)
        {
            cnt++;
            v[cnt]=a*s;
            w[cnt]=b*s;
        }
    }
    for(int i=1;i<=cnt;i++)
    {
        for(int j=V;j>=v[i];j--)
        {
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }
    cout<<f[V];
    return 0;
}

104用单调队列优化
用单调队列优化的理由是,每一次多重背包,相当于前sv个(s是每一种物品的数量)体积状态转移而来,而这就变成了一个滑动窗口求最大值问题,但是最后得到的结果值还需要加上sw。
这里队列用数组模拟,用deque过不了

#include <cstring>
#include <iostream>
#include <algorithm>
#include <deque>

using namespace std;

const int N = 20010;

int n, m;
int f[N], g[N], q[N];
deque<int> d;

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
    {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof f);
        for (int j = 0; j < v; j ++ )
        {
            int hh=0,tt=-1;
            for (int k = j; k <= m; k += v)
            {
                if (hh <= tt && q[hh] < k - s * v) hh ++ ;
                while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt -- ;
                q[ ++ tt] = k;
                f[k] = g[q[hh]] + (k - q[hh]) / v * w;
                /*f[k]=g[k];
                if(d.size() && d.front() < k - s * v) 
                    d.pop_front();
                while (d.size() && g[d.back()] - (d.back() - j) / v * w <= g[k] - (k - j) / v * w) 
                    d.pop_back();
                d.push_back(k);
                if(d.size())
                    f[k]=max(f[k],g[d.front()] + (k - d.front()) / v * w);*/
                
            }
        }
    }

    cout << f[m] << endl;

    return 0;
}

二维费用背包问题

#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int main()
{
    int n,V,M;
    cin>>n>>V>>M;
    for(int i=0;i<n;i++)
    {
        int v,m,w;
        cin>>v>>m>>w;
        for(int i=V;i>=v;i--)
        {
            for(int j=M;j>=m;j--)
            {
                f[i][j]=max(f[i][j],f[i-v][j-m]+w);
            }
        }
    }
    cout<<f[V][M];
    return 0;
}

分组背包问题
i和j表示的是维度
而k表示的是可选决策方案
所以三者的顺序绝对不能混淆。
就像floyd的k要在外层循环,是因为三维变量优化成了二维一样。

#include<iostream>
using namespace std;
const int N=1010;
int f[N];
int v[N];
int w[N];
int main()
{
    int n,V;
    cin>>n>>V;
    for(int i=0;i<n;i++)
    {
        int s;
        cin>>s;
        for(int j=1;j<=s;j++)
        {
            cin>>v[j]>>w[j];
        }
        for(int k=V;k>=0;k--)
            for(int j=1;j<=s;j++)
            {
                if(k>=v[j])
                    f[k]=max(f[k],f[k-v[j]]+w[j]);
            }
    }
    cout<<f[V];
    return 0;
}

有依赖的背包问题
由于物品之间有依赖,我们把每个依赖连成一条边进行树形dp。
把一个结点u的按子树分类,每个子树又按照子树能表示的体积继续划分。
在最后把这个结点u塞进背包,如果放不下了,则整个子树都没有价值。
因为没初始化h数组调试了好久!千万别粗心了,写数组模拟邻接表h数组必须初始化为-1。

#include<bits/stdc++.h>
using namespace std;
const int N=110;
int f[N][N];
int h[N],e[N],ne[N],idx;
int w[N],v[N];
void add(int a,int b)
{
    e[idx]=b;
    ne[idx]=h[a];
    h[a]=idx++;
}
int n,V;
void dfs(int u)
{
   
    for(int i=h[u];~i;i=ne[i]) 
    {
        dfs(e[i]);
        
        for(int j=V-v[u];j>=v[e[i]];j--)//每个e[i]都组成一个分组,分组中的每个物品体积是下一层所能组成的体积
        {
            for(int k=0;k<=j;k++)
            {
                f[u][j]=max(f[u][j],f[u][j-k]+f[e[i]][k]);
            }
        }
    }
    for(int i=V;i>=v[u];i--)//01背包,把u放进去
    {
        f[u][i]=f[u][i-v[u]]+w[u];
    }
    for(int i=0;i<v[u];i++)//u放不进去,u的子结点也不能放了
    {
        f[u][i]=0;        
    }
}
int main()
{
    
    cin>>n>>V;
    int root;
    memset(h,-1,sizeof h);//你写数组模拟邻接表必须写这个啊
    for(int i=1;i<=n;i++)
    {
        int p;
        cin>>v[i]>>w[i]>>p;
        if(p==-1) root=i;
        else
        {
            add(p,i);
        }
    }
    dfs(root);
    cout<<f[root][V];
    return 0;
}

背包问题求方案数
更新的时候用一个cnt记录方案数

#include<iostream>
using namespace std;
const int N=10010;
int cnt[N];
int f[N];
int mod=1000000007;
int main()
{
    int n,V;
    cin>>n>>V;
    for(int i=0;i<V;i++)
    {
        cnt[i]=1;
    }
    for(int i=0;i<n;i++)
    {
        int v,w;
        cin>>v>>w;
        for(int j=V;j>=v;j--)
        {
            int value=f[j-v]+w;
            if(value>f[j])
            {
                f[j]=value;
                cnt[j]=cnt[j-v];
            }
            else if(value==f[j])
            {
                cnt[j]=(cnt[j]+cnt[j-v])%mod;
            }
        }
    }
    cout<<cnt[V];
    return 0;
    
}

背包问题求具体方案
在最后倒推即可

#include<iostream>
using namespace std;
const int N=1010;
int f[N][N],v[N],w[N];
int main()
{
    int n,V;
    cin>>n>>V;
    for(int i=1;i<=n;i++)
    {
        cin>>v[i]>>w[i];
    }
    for(int i=n;i>=1;i--)
    {
        for(int j=0;j<=V;j++)
        {
            f[i][j]=f[i+1][j];
            if(j>=v[i])
            {
                f[i][j]=max(f[i+1][j],f[i+1][j-v[i]]+w[i]);
            }
        }
    }
    int vol=V;
    for(int i=1;i<=n;i++)
    {
        if(f[i][vol]==f[i+1][vol-v[i]]+w[i]&&vol>=v[i])
        {
            cout<<i<<" ";
            vol-=v[i];
        }
    }
    return 0;
}

01背包的数据范围问题
1.相比体积而言,价值的范围比较小
我们就将01背包的第二维变成价值。
dp[i][j]=min(dp[i-1][j],dp[i-1][j-w[i]]+v[i])
最后寻找dp[i][j]<=V最大的j

void solve()
{
	memset(dp,0x3f,sizeof dp);
	dp[0][0]=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<=n*W;j++)
		{
			if(j<v[i])
				dp[i][j]=dp[i-1][j];
			else
				dp[i][j]=min(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
		}
	}
	int res=0;
	for(int i=0;i<=n*W;i++)
	{
		if(dp[n][i]<=W)
			res=i;
	}
	cout<<res;
} 在这里插入代码片

2.体积和价值的范围都较大
折半查找

void solve()
{
	int n2=n/2;
	for(int i=0;i<1<<n2;i++)//二进制枚举每种选取情况 
	{
		ll sw=0,sv=0;
		for(int j=0;j<n2;j++)
		{
			if(i>>j&1)
			{
				sw+=w[j];
				sv+=v[j];
			}
		}
		pa[i]=make_pair(sw,sv);//之后好排序 
	} 
	
	//如果一个物品体积比另一个物品大,价值又低,就去掉
	sort(pa,pa+(1<<n2));
	int m=1;
	for(int i=1;i<1<<n2;i++)
	{
		if(pa[m-1].second<pa[i].second)
		{
			pa[m++]=pa[i];
		}
	} 
	
	//枚举后半部分并求解
	ll res=0;
	for(int i=0;i<1<<(n-n2);i++)
	{
		ll sw=0,sv=0;
		for(int j=0;j<n-n2;j++)
		{
			if(i>>j&1)
			{
				sw+=w[n2+j];
				sv+=v[n2+j];
			}
		}
		if(sw<=W)
		{
			ll tv=(lower_bound(pa,pa+m,make_pair(W-sw,0x3f)))-1)->second;//查找小于W-sw的,v最大的pa
			//这个pair用的很巧妙学着点
			res=max(res,sv+tv) 
		}
		cout<<res;
	} 
	
} 

背包问题恰好、至少、至多的处理方法

最长上升子序列

#include<iostream>
#include<cstring>
using namespace std;
const int N=10010;
typedef long long LL;
LL a[N];
LL f[N]; 
int main()
{
	LL n,mx;
	cin>>n;
	for(LL i=0;i<n;i++)
	{
		cin>>a[i];
	}
	//memset(f,1,sizeof f);是不对的只能初始化0或者-1
	for(int i=0;i<n;i++)
	{
		f[i]++;
	} 
	mx=0;
	for(LL i=0;i<n;i++)
	{
		for(LL j=0;j<i;j++)
		{
			if(a[i]>a[j])
				f[i]=max(f[i],f[j]+1);
		}
		
		mx=max(mx,f[i]);
	}
	cout<<mx;
	return 0;
}

最长上升子序列的优化
这是个常使用的贪心策略
q[i]表示包含i个数的最长上升子序列的末尾值
这个末尾值一定是单调递增的
这样就可以二分查找最后一个数应当插入的,序列结尾比他小的值的最大值所在的序列。

#include<bits/stdc++.h>
using namespace std;
int a[100001];
int q[100001];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    int len=0;
    for(int i=1;i<=n;i++)
    {
        int l=0,r=len;
        while(l<r)
        {
            int mid=l+r+1>>1;
            if(q[mid]<a[i])
                l=mid;
            else
                r=mid-1;
        }
        len=max(len,r+1);
        q[r+1]=a[i];
        
    }
    cout<<len;
    
    return 0;
}

最长公共上升子序列

#include<bits/stdc++.h>
using namespace std;
const int N=3001;
int a[N],b[N],f[N][N];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    for(int i=1;i<=n;i++)
    {
        cin>>b[i];
    }
    for(int i=1;i<=n;i++)
    {
        int maxv=1;
        for(int j=1;j<=n;j++)
        {
            f[i][j]=f[i-1][j];
            if(a[i]==b[j])
                f[i][j]=max(f[i][j],maxv);
            if(a[i]>b[j])
                maxv=max(maxv,f[i-1][j]+1);
            
        }
    }
    int res=0;
    for(int i=1;i<=n;i++)
    {
        res=max(res,f[n][i]);
    }
    cout<<res;
    return 0;
}

导弹防御系统

#include<bits/stdc++.h>
using namespace std;
int a[55];
int up[55],down[55];
int ans;
int n,depth;
bool dfs(int depth,int u,int su,int sd)
{
    if(su+sd>=depth)//如果是n的话你的剪枝是不到位的
    {
        return false;
    }
    if(u==n)
    {
        return true;
    }
    
    int k=0;
    while(k<su&&up[k]>=a[u]) k++;
    int t=up[k];
    up[k]=a[u];
    if(k<su)
        if(dfs(depth,u+1,su,sd)) return true;
    else
        if(dfs(depth,u+1,su+1,sd)) return true;
    up[k]=t;
    
    k=0;
    while(k<sd&&down[k]<=a[u]) k++;
    t=down[k];
    down[k]=a[u];
    if(k<sd)
        if(dfs(depth,u+1,su,sd)) return true;
    else
        if(dfs(depth,u+1,su,sd+1)) return true;
    down[k]=t;
    
}
int main()
{
    while(cin>>n,n)
    {
        ans=n;
        for(int i=0;i<n;i++)
        {
            cin>>a[i];
        }
        depth=0;
        while(!dfs(depth,0,0,0)) depth++;
        cout<<depth<<endl;
    }
    return 0;
}

划分数
n的m划分
dp[m][n]=dp[m-1][n]+dp[m][n-m]
当某个集合包含0,相当于n的m-1划分
当某个集合不包含0,等价于在每个集合抽走1后的划分,即n-m的m划分

组合数学中常用组合数介绍

多重部分和
用dp[i][j]表示枚举到第i个数,他们的和为j是否能实现。
dp[i][j]=dp[i][j]|dp[i][j-ka[i]]
相当于只要能构成和为j,就改变dp[i][j]的状态为1
这个和枚举没啥区别。

一个数组用来放bool值会造成浪费,因此我们可以用dp[i][j]存放用前i种数加和得到j时第i种数剩余多少个。
如果i-1个数能得到j,直接等于m i _{i} i.
否则dp[i][j]=dp[i][j-a i _{i} i]-1
如果用完了也不能得到,就赋值为-1
由于公式里有i也有i-1,我们用完全背包的方式正向枚举体积降维。

void solve()
{
	memset(dp,-1,sizeof dp);
	dp[0]=0;
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<=k;j++)
		{
			if(dp[j]>0)
			{
				dp[j]=m[i];
			}
			else if(j<a[i]||dp[j-a[i]]<=0)
			{
				dp[j]=-1;
			}
			else
			{
				dp[j]=dp[j-a[i]]-1;
			}
		}
	}
	if(dp[k]>=0)
		cout<<"yes";
}

Coins
这道题求1到m能被拼成的面值有多少个,和上面的多重部分和是一样的,最后加一个循环统计f[i]为1的情况有多少个。

for(int i=1;i<=n;i++)
{
	for(int j=0;j<=m;j++)
	{
		used[j]=0;
	}
	for(int j=a[i];j<=m;j++)
	{
		if(!f[j]&&f[j-a[i]]&&used[j-a[i]]<c[i])
		{
			f[j]=1;
			used[j]=used[j-a[i]]+1;
		}
	}
}

多重集组合数

dp[i][j]表示从前i种物品取出j个的组合数
dp[i][j]= ∑ k = 0 m i n ( j , a [ i ] ) d p [ i ] [ j − k ] \sum_{k=0}^{min(j,a[i])} dp[i][j-k] k=0min(j,a[i])dp[i][jk]
这个式子可以化简变形

若j ⩽ \leqslant a i _{i} i

∑ k = 0 m i n ( j , a [ i ] ) d p [ i ] [ j − k ] \sum_{k=0}^{min(j,a[i])} dp[i][j-k] k=0min(j,a[i])dp[i][jk]

= ∑ k = 0 j d p [ i ] [ j − k ] \sum_{k=0}^{j} dp[i][j-k] k=0jdp[i][jk]

=dp[i][j]+dp[i][j-1]+…+dp[i][0]

= ∑ k = 0 j − 1 d p [ i ] [ j − 1 − k ] + d p [ i ] [ j ] \sum_{k=0}^{j-1} dp[i][j-1-k]+dp[i][j] k=0j1dp[i][j1k]+dp[i][j]

若j ⩾ \geqslant a i _{i} i

∑ k = 0 m i n ( j , a [ i ] ) d p [ i ] [ j − k ] \sum_{k=0}^{min(j,a[i])} dp[i][j-k] k=0min(j,a[i])dp[i][jk]

= ∑ k = 0 a i d p [ i ] [ j − k ] \sum_{k=0}^{a_{i}} dp[i][j-k] k=0aidp[i][jk]

=dp[i][j]+dp[i][j-1]+…+dp[i][ j − a i j-a_{i} jai]+dp[i][ j − 1 − a i j-1-a_{i} j1ai]
-dp[i][ j − 1 − a i j-1-a_{i} j1ai]

= ∑ k = 0 j − 1 d p [ i ] [ j − 1 − k ] + d p [ i ] [ j ] \sum_{k=0}^{j-1} dp[i][j-1-k]+dp[i][j] k=0j1dp[i][j1k]+dp[i][j]

dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i][ j − 1 − a i j-1-a_{i} j1ai]

复杂度下降到O(nm)

还要注意初始化,一个都不取得到0个也算一种方案

void solve()
{
	for(int i=0;i<=n;i++)
	{
		dp[i][0]=1;
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(j-1-a[i]>=0)//也就是j比a[i]大
			{
				dp[i][j]=(dp[i-1][j]+dp[i][j-1]-dp[i][j-1-a[i]])%M;
			} 
			else
			{
				dp[i][j]=(dp[i-1][j]+dp[i][j-1])%M;
			}
		}
	}
}

这里讲了背包问题和计数dp,其他的dp方式将在3.4讲。
背包问题题单
能量石
这道题贪心性质很好想,只要做过国王游戏,不难把两个能量石交换吃的先后顺序的式子列出来。

把吃的时间看成体积,得到的能量看成价值,不难发现是背包解决。但是时间这个体积是没有上限的。可以把吃所有能量石的时间和看做上限,到一定时间以后,有些能量石就会看成只花费吃的时间,不会增加价值的物品,对我们结论的最大价值是没有影响的。

#include<bits/stdc++.h>
using namespace std;
const int N=10010;
int f[N];
struct Stone
{
    int s,e,l;
    bool operator < (const Stone &w) const
    {
        return s*w.l<w.s*l;
    }
}S[N];
int main()
{
    int t;
    cin>>t;
    int cas=1;
    while(t--)
    {
        int n;
        cin>>n;
        int m=0;
        for(int i=0;i<n;i++)
        {
            cin>>S[i].s>>S[i].e>>S[i].l;
            m+=S[i].s;
        }
        sort(S,S+n);
        memset(f,-0x3f,sizeof f);
        f[0]=0;
        //之所以用01背包,是因为能量已经到0了我们就可以不用选他了
        for(int i=0;i<n;i++)
        {
            for(int j=m;j>=S[i].s;j--)
            {
                f[j]=max(f[j],f[j-S[i].s]+S[i].e-(j-S[i].s)*S[i].l);//他的价值就是他拥有的能量减去已经耗费的能量
            }
        }
        int res=0;
        for(int i=0;i<=m;i++)
            res=max(res,f[i]);
        cout<<"Case #"<<cas<<": "<<res<<endl;
        cas++;
    }
    return 0;
}

货币系统
最重要的是想到,只要能被表示出来,就被没有必要有这个数,这样一个性质。
代码中用完全背包凑数,凑不出来的res++即可。

#include<bits/stdc++.h>
using namespace std;
const int N=25010;
int a[N],f[N];
int main()
{
    int p;
    cin>>p;
    while(p--)
    {
        
        int n;
        cin>>n;
        memset(f,0,sizeof f);
        memset(a,0,sizeof a);
        f[0]=1;
        int k=0;
        for(int i=1;i<=n;i++)
        {
            cin>>a[i];
            k=max(a[i],k);
        }
        for(int i=1;i<=n;i++)
        {
            for(int j=a[i];j<=k;j++)
            {
                f[j]+=f[j-a[i]];
            }
        }
        int res=0;
        for(int i=1;i<=n;i++)
        {
            if(f[a[i]]==1)
                res++;
        }
        cout<<res<<endl;
       
    }
    return 0;
}

金明的预算方案
乍一看是个有依赖的背包问题,可是这是个森林,每棵树只有两层。
观察到每个主件最多有2个附件,即总共一棵树有最多四种情况:无子树,有左,有右,左右都有。
把整个看做一个分组背包问题解决,在分成每一组时加入主件,列举最多4个决策。
思路并不麻烦,但这道题实现起来比较麻烦,要想到如何把附件链接到主件存储,这里用的vector<pair<int,int>>,可以用unordered_map。
另外一个难点是用二进制去枚举各种附件的情况,最后求最大值。

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
const int M=32010;
int f[M];
int w[N];
pair<int,int> a[N];
vector<pair<int,int> > b[N];
int main()
{
    int m,n;
    cin>>m>>n;
    for(int i=1;i<=n;i++)
    {
        int v,p,q;
        cin>>v>>p>>q;
        p*=v;
        if(q==0)
        {
            a[i].first=v;
            a[i].second=p;
        }
        else
        {
            b[q].push_back({v,p});
        }
    }
    for(int i=1;i<=n;i++)
    {
        for(int k=m;k>=0;k--)//枚举体积
            for(int j=0;j<1<<b[i].size();j++)//枚举每一组的各种物品
            {
                int v=a[i].first;
                int w=a[i].second;
                for(int g=0;g< b[i].size();g++)//枚举各种物品中细分的选法
                {
                    if(j>>g&1)
                    {
                        v+=b[i][g].first;
                        w+=b[i][g].second;
                    }
                } 
                if(k>=v)
                {
                    f[k]=max(f[k],f[k-v]+w);
                }
            }
    }
    cout<<f[m];
    return 0;
}

数字拆分的两道题目
1.数字组合
这是个01背包问题,因为题目给出的那几个数只能用一次。

for(int i=1;i<=n;i++)
{
	for(int j=m;j>=a[i];j--)
	{
		f[j]+=f[j-a[i]];
	}
}

2.自然拆分lunatic版
拆成一堆正整数相加,每个正整数一定可以用多次。
所以是完全背包问题呢。

memset(f,0,sizeof f);
f[0]=1;
for(int i=1;i<=n;i++)
{
	for(int j=i;j<=n;j--)
	{
		f[j]=(f[j]+f[j-i])%mod;
	}
}

Jury Compromise

计数dp题单

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值