[动态规划] 背包 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\) ~ \(n - 1\),共 \(n - 1\) 个物品,价值就是数值本身。完全背包即可。

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[j - 1][d - a[i]][p - b[i]] \]

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

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

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

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

\[f[j][k] = max{ f[j][k], f[j - 1][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[i - 1][j] | f[i - 1][j - v[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;
}
posted @ 2024-05-11 11:41  PassName  阅读(8)  评论(0编辑  收藏  举报