在这片梦想之地,不堪回首的过去像泡沫一样散去,不|

PassName

园龄:3年1个月粉丝:32关注:16

[动态规划] 背包 dp

背包 dp

AcWing 278. 数字组合

n 个数就是 n 个物品,每个物品的价值就是它本身的数值,只能用一次,要求价值和为 m 的方案数。直接 01 背包即可。

int n, m;
int a[N], f[M];

signed main()
{
    cin >> n >> m;
    for (rint i = 1; i <= n; i++) cin >> a[i];
    memset(f, 0, sizeof f);
    f[0] = 1;
    for (rint i = 1; i <= n; i++)
        for (rint j = m; j >= a[i]; j--)
            f[j] += f[j - a[i]];
    cout << f[m] << endl;
    return 0;
}

AcWing 279. 自然数拆分

若干个正整数就是若干个物品,要想能相加为 n,且至少两个数相加,能用上的数就是 1 ~ n1,共 n1 个物品,价值就是数值本身。完全背包即可。

int n;
int f[N];

signed main()
{
    cin >> n;
    memset(f, 0, sizeof f);
    f[0] = 1;
    for (rint i = 1; i <= n - 1; i++)
        for (rint j = i; j <= n; j++)
            f[j] = (f[j] + f[j - i]) % mod;

    cout << f[n] << endl;

    return 0;
}

UVA323 Jury Compromise

本题是一个 01 背包问题,我们将 n 个人看作 n 个物品,那么每个物品会有三个体积:

    1. 人数,每个候选人都是 1 个人,最终要选出 m 个人
    1. 辩方得分,即辩方给每个候选人打的分数 a[i]
    1. 控方得分,即控方给每个候选人打的分数 b[i]

因此我们需要依次考虑每个候选人是否选入评审团,当外层循环到阶段 i 时,表示已经考虑了前 i 个候选人的入选情况,用 bool 数组 f[j][d][p] 表示已有 j 人被选入评审团,当前辩方总分为 d、控方总分为 p 的状态是否可行。

i 个候选人有选和不选两种情况,得出状态转移方程:

f[j][d][p]=f[j][d][p]|f[j1][da[i]][pb[i]]

起始状态 f[0][0][0]=1,目标是 f[m][d][p]=1,要求 |dp| 尽量小,d+p 尽量大。

到此我们初步分析出了一个算法,但是并没有很好的利用价值这一维度,我们可以进一步优化,我们可以将每个候选人的辩方、控方双方得分的差 a[i]b[i] 作为体积之一,把辩方、控方双方得分的和作为该物品
的价值。

当外层循环到 i 时,设 f[j][k] 表示已经在前 i 个候选人中选出了 j 个,此时辩方与控方总分的差为 k 时,辩方与控方总分的和的最大值。

同样有选和不选两种情况,状态转移方程:

f[j][k]=maxf[j][k],f[j1][k(a[i]b[i])]+(a[i]+b[i])

起始状态 f[0][0]=0,目标是 f[m][k],满足 |k| 尽量小,当 |k| 相同时 f[m][k] 尽量大。

最终还要输出具体方案,用一个 d[i][j][k] 表示外层循环到 i 时,状态 f[j][k] 是从哪个候选人转移过来的。递归找出整个方案即可。

int n, m;
int f[M][K]; 
int d[N][M][K];
//表示 f[i][j][k] 是从哪个候选人转移过来的
int a[N], b[N]; 
//每个候选人的辩方、控方得分
vector<int> path; 
//选择的候选人编号
int suma, sumb; 
//辩方、控方总分

void get_path(int i, int j, int k) 
//从最优状态回推方案
{
    if (!j) return; 
	//回推完所有候选人结束程序
    int last = d[i][j][k];
    get_path(last - 1, j - 1, k - (a[last] - b[last])); 
	//继续递归
    path.push_back(last); 
	//将当前候选人加入方案
    suma += a[last], sumb += b[last]; 
	//累加辩方、控方总分
}

signed main()
{
    int T = 1;
    while (cin >> n >> m && n || m)
    {
        for (rint i = 1; i <= n; i++) cin >> b[i] >> a[i];
        memset(f, -0x3f, sizeof f);
        f[0][400] = 0; //f[0][0] -> f[0][400] (k 平移 400)
        //01背包
        for (rint i = 1; i <= n; i++)
        {
            for (rint j = m; j > 0; j--)
            {
                for (rint k = 0; k <= 800; k++)
                {
                    //不选 i
                    d[i][j][k] = d[i - 1][j][k];

                    //选 i
                    if (k - (a[i] - b[i]) < 0 || k - (a[i] - b[i]) > 800) continue; 
					//状态不合法直接跳过
                    if (f[j][k] < f[j - 1][k - (a[i] - b[i])] + (a[i] + b[i])) 
					//如果辩方、控方总和之和更大,更新
                    {
                        f[j][k] = f[j - 1][k - (a[i] - b[i])] + (a[i] + b[i]); 
						//状态转移
                        d[i][j][k] = i; //记录从哪个候选人转移过来
                    }
                }				
			}
        }

        //找出最优方案对应的 k
        int res = 0;
        for (rint k = 0; k <= 400; k++) // k 尽可能的小
        {
            if (f[m][400 + k] >= 0 && f[m][400 + k] >= f[m][400 - k]) 
			//辩方、控方总分的和尽量大
            {
                res = 400 + k; //选双方总和的和较大的一个 k
                break; //第一个有解的 k 一定最小
            }
            else if (f[m][400 - k] >= 0)
            {
                res = 400 - k;
                break;
            }			
		}

        path.clear(); //清空方案

        suma = sumb = 0; //重置总分
        get_path(n, m, res); //从最优状态回推方案

        //输出
        printf("Jury #%lld\n", T++);
        printf("Best jury has value %d for prosecution and value %lld for defence:\n", sumb, suma);
        for (rint i = 0; i < path.size(); i++) printf(" %lld", path[i]);
        printf("\n\n");
    }
    return 0;
}

AcWing 281. 硬币

本题问的是有几个结果是可以组成的,是一个可行性问题,不是一个最优性问题。可以看出是一个多重背包问题。因此设 bool 数组 f[i][j] 表示前 i 种硬币能否拼成面值 j

可以把多重背包拆成01背包来做,状态转移方程:

f[i][j]=f[i1][j]|f[i1][jv[i]]

但是这样时间复杂度太高了,需要进行优化,用二进制拆分法。

int n, m;
int a[N]; 
//a[i] 表示第 i 个硬币的面值,s[i] 表示第 i 个硬币的数量
int v[N], cnt; 
//二进制拆分后每个物品的面值
bool f[M]; 
//设 f[i][j] 表示前 i 个硬币能否拼成面值 j,降掉 i 维

signed main()
{
    while (cin >> n >> m, n || m)
    {
        for (rint i = 1; i <= n; i++) cin >> a[i];
        memset(f, 0, sizeof f);
        f[0] = 1;

        //二进制拆分
        cnt = 0;
        for (rint i = 1; i <= n; i++)
        {
            int s;
            cin >> s;
            int k = 1;
            while (k <= s)
            {
                cnt++;
                v[cnt] = a[i] * k;
                s -= k;
                k *= 2;
            }
            if (s > 0)
            {
                cnt++;
                v[cnt] = a[i] * s;
            }
        }

        //01背包
        for (rint i = 1; i <= cnt; i++)
            for (rint j = m; j >= v[i]; j--)
                f[j] |= f[j - v[i]];

        int ans = 0;
        for (rint i = 1; i <= m; i++) ans += f[i];
        cout << ans << endl;
    }
    
    return 0;
}

本文作者:PassName

本文链接:https://www.cnblogs.com/spaceswalker/p/18186213

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   PassName  阅读(13)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起