背包基础篇(01、完全、分组、多重、混合)

背包基础篇(01、完全、分组、多重、混合)

问题链接
2. 01背包问题 - AcWing题库

11. 背包问题求方案数 - AcWing题库

12. 背包问题求具体方案 - AcWing题库

8. 二维费用的背包问题 - AcWing题库
3. 完全背包问题 - AcWing题库
9. 分组背包问题 - AcWing题库
4. 多重背包问题 I - AcWing题库

5. 多重背包问题 II - AcWing题库
7. 混合背包问题 - AcWing题库


一、01 背包

01 背包:每件物品只能选一次,物品有重量v[i],价值w[i],背包总容量V,求最大总价值。

1、二维 DP

状态定义:dp[i][j]:前i件物品,背包容量为j时的最大价值。

转移方程:

不选第 i件: d p i j = d p i − 1 j dpij=dpi-1j dpij=dpi−1j

不选第 i件: d p i j = d p i − 1 j − v \[ i ] + w i dpij=dpi-1j-v\[i]+wi dpij=dpi−1j−v\[i]+wi (需要背包容量j 大于等于 v[i],表示可以装下这个物品 )

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int v1[N],w1[N];  // v1[]重量,w1[]价值
int dp[N][N];     // dp[i][j]前i个物品,背包容量j的最大价值

int main(){
	int n,v;
	cin>>n>>v;
	// 读入n件物品:重量、价值
	for(int i=1;i<=n;i++){
		cin>>v1[i]>>w1[i];
	}

	// 遍历每件物品
	for(int i=1;i<=n;i++){
		// j倒序也能跑,但二维推荐正序
		for(int j=v;j>=0;j--){
			dp[i][j]=dp[i-1][j];  // 情况1:不选第i件物品
			if(v1[i]<=j)          // 背包装得下才可以选
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-v1[i]]+w1[i]); // 取选/不选的最大值
		}
	}
	cout<<dp[n][v]; // n件物品,容量v的最大答案
	return 0;
}

2、一维 DP

二维压缩成一维,j 必须倒序遍历,防止物品被多次选取,如果是正序遍历的话,容量是从小的开始的,会导致一个物品在放入之后,随着容量的增大,会被再次放入,会变成完全背包问题。

状态定义:dp[j]:容量j背包的最大价值。

转移方程: d p j = m a x ( d p j , d p j − v \[ i ] + w i ) dpj=max(dpj,dpj-v\[i]+wi) dpj=max(dpj,dpj−v\[i]+wi)

关键点:j从总容量 Vv[i],倒序!

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
int main(){
	int n,v1;
	cin>>n>>v1;
	vector<int>v(n+1),w(n+1);
	for(int i=0;i<n;i++){
		cin>>v[i]>>w[i];
	}
	vector<int>dp(v1+1);
	for(int i=0;i<n;i++){
		for(int j=v1;j>=v[i];j--){
			dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
		}
	}
	cout<<dp[v1];
	return 0;
}

01 背包一维必须倒序 j;完全背包是正序 j。


3、不考虑价值,恰好装满的方案数

  • 内层循环:j 从 V 倒序到 v[i]
  • 转移:dp[j] = (dp[j] + dp[j‑v[i]]) % MOD
  • ①恰好装满 V:答案 dp[V]
  • ②体积不超过 V:答案 sum(dp[0]~dp[V]) % MOD

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
int dp[1003];
const int N=1e9+7;
int v1[1003],w1[1003];
int main(){
	int n,v;
	cin>>n>>v;
	dp[0]=1;
	for(int i=0;i<n;i++){
		cin>>v1[i]>>w1[i];
	}
	for(int i=0;i<n;i++){
		for(int j=v;j>=v1[i];j--){
			dp[j]=(dp[j]+dp[j-v1[i]])%N;
		}
	}
	cout<<dp[v];
	return 0;
}

4、体积不超过 V,求能拿到最大价值的方案数

题意:先选出总价值最大的选法,统计一共有多少种这样的选法。 背包容量 V,物品体积不超过 V,不一定非要把背包装满。 需要两个数组:

  • dp[j]:容量 j 的背包可以获得的最大价值
  • cnt[j]:容量 j,达到最大价值f[j]的方案数量

初始化:cnt其他都是0,只有 cnt[0]=1,因为背包容量为0,是只有一种方案数,就是什么都不装。

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
int const N=1e9+7;
int dp[1003];
int cnt[1003];
int v1[1003],w1[1003];
int main(){
	int n,v;
	cin>>n>>v;
	for(int i=0;i<n;i++){
		cin>>v1[i]>>w1[i];
	}
	for(int i=0;i<=v;i++){
		cnt[i]=1;
	}
	for(int i=0;i<n;i++){
		for(int j=v;j>=v1[i];j--){
			if(dp[j]<dp[j-v1[i]]+w1[i]){
				dp[j]=dp[j-v1[i]]+w1[i];
				cnt[j]=cnt[j-v1[i]]%N;
			}
			else if(dp[j]==dp[j-v1[i]]+w1[i]){
				cnt[j]=(cnt[j]+cnt[j-v1[i]])%N;
			}
		}
	}
	cout<<cnt[v];
	return 0;
}

5、背包问题求具体方案

二维 dp 从后往前递推,然后正向循环回溯输出选择物品编号,用来输出 01 背包选的物品。

数组含义

dp[i][j]:只考虑i ~ n 号物品 ,背包容量为j时,可以得到的最大价值

注意:in 往下算,代表:从第 i 件到最后一件物品。

v1[i],w1[i]:第i件物品体积、价值

📝DP 递推部分(i 从 n 到 1)

复制代码
for(int i=n;i>=1;i--){
    for(int j=v;j>=0;j--){
        dp[i][j]=dp[i+1][j]; // 情况1:不选第i件物品
        if(j>=v1[i])
            dp[i][j]=max(dp[i+1][j], dp[i+1][j-v1[i]]+w1[i]);
    }
}
  • dp[i][j] = dp[i+1][j]不选 i 号物品 ,那么就依赖后面i+1~n物品的结果。
  • 如果背包装得下 i 物品( j>=v1[i]): 对比两种选择:
    1. 不选 i:dp[i+1][j]
    2. 选 i:dp[i+1][j‑v1[i]] + w1[i],选完 i 之后剩下容量j‑v1[i],再看后面i+1~n物品
  • 取两者 max 赋值给dp[i][j]

边界:dp[n+1][j],没有物品可以选,全部是 0,全局数组自动初始 0。
和普通二维背包对比: 普通二维:dp[i][j]代表前 i 件物品(1~i) ,i 从 1 到 n。 这份代码:dp[i][j]代表i~n 后缀物品,i 从 n 倒推到 1。

✂回溯部分:找出到底选了哪些物品

复制代码
int vv=v;
for(int i=1;i<=n;i++){
    if(vv<=0) break;
    if(vv>=v1[i] && dp[i][vv]==dp[i+1][vv-v1[i]]+w1[i]){
        cout<<i<<" ";
        vv-=v1[i];
    }
}

逻辑 :从第 1 件物品开始依次判断,这件物品有没有被选上。 条件:

复制代码
dp[i][vv] == dp[i+1][vv‑v1[i]] + w1[i]

含义 :当前最优价值 =【选 i 物品后的价值】,说明最优解里面选了 i 号物品

  • 如果成立:输出物品编号i,背包剩余容量减去该物品体积 vv -= v1[i]
  • 如果不成立:说明最优解没有选 i,直接看下一个 i+1

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int v1[N],w1[N];
int dp[N][N];
int main(){
	int n,v;
	cin>>n>>v;
	for(int i=1;i<=n;i++){
		cin>>v1[i]>>w1[i];
	}
	for(int i=n;i>=1;i--){
		for(int j=v;j>=0;j--){
			dp[i][j]=dp[i+1][j];
			if(j>=v1[i])
				dp[i][j]=max(dp[i+1][j],dp[i+1][j-v1[i]]+w1[i]);
		}
	}
	int vv=v;
	for(int i=1;i<=n;i++){
		if(vv<=0)
			break;
		if(vv>=v1[i]&&dp[i][vv]==dp[i+1][vv-v1[i]]+w1[i]){
			cout<<i<<" ";
			vv-=v1[i];
		}
	}
	return 0;
}

6、二维费用的背包问题

每件物品有两种消耗(双重约束),本题:

  • 消耗 1:体积 (v1i),背包体积上限 v
  • 消耗 2:重量 (m1i),背包重量上限 m
  • 物品价值 (w1i),每件物品最多选 1 次 求在体积≤v 并且 重量≤m条件下的最大价值。

普通 01 背包只有 1 个约束(体积);二维费用背包增加第二个约束条件,dp 数组多一维。

dp 状态定义

dp[j][k]:总体积用了j,总重量用了k,可以获得的最大价值。

状态转移

复制代码
dp[j][k]=max(dp[j][k],dp[j‑v1[i]][k‑m1[i]]+w1[i]);
  • 不选该物品:保留原来dp[j][k]
  • 选该物品:体积减去(v1i),重量减去(m1i),再加上当前物品价值。

循环关键点

复制代码
for(int i=0;i<n;i++)
    for(int j=v;j>=v1[i];j--)    //体积维度:倒序
        for(int k=m;k>=m1[i];k--) //重量维度:也要倒序

两个费用维度都必须倒序遍历

  • 一维 01 背包:只需要容量倒序,避免重复选同一个物品。
  • 二维费用 01 背包:两维全部倒序。 只要有一维写成正序,就会变成完全背包逻辑,同一个物品可以多次选取,直接 WA。

j 循环和 k 循环可以互换先后顺序,结果不变。

初始化两种模式

  1. 不要求装满(本题代码) 全局 dp 数组默认 0;允许体积、重量不用耗尽,求最大价值。输出dp[v][m]
  2. 要求恰好装满 dp 全部初始为负无穷,dp[0][0]=0。代表体积 0 重量 0 时价值为 0,其余状态不可达。

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int dp[N][N]; 
int v1[N],m1[N],w1[N];
int main(){
	int n,v,m;
	cin>>n>>v>>m;
	for(int i=0;i<n;i++){
		cin>>v1[i]>>m1[i]>>w1[i];
	}
	for(int i=0;i<n;i++){
		for(int j=v;j>=v1[i];j--){
			for(int k=m;k>=m1[i];k--){
				dp[j][k]=max(dp[j][k],dp[j-v1[i]][k-m1[i]]+w1[i]);
			}
		}
	}
	cout<<dp[v][m];
	return 0;
}

二、完全背包

特点:物品可以无限选取,每件物品能选多次

状态定义:dpj 表示背包容量为 j 时能获得的最大价值

1、一维DP

转移方程: d p j = m a x ( d p j , d p j − v \[ i ] + w i ) dpj=max(dpj,dpj-v\[i]+wi) dpj=max(dpj,dpj−v\[i]+wi)

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
int dp[1009];
int v1[1009],w1[1009];
int main(){
	int n,v;
	cin>>n>>v;
	for(int i=0;i<n;i++){
		cin>>v1[i]>>w1[i];
	}
	for(int i=0;i<n;i++){
		for(int j=v1[i];j<=v;j++){
			dp[j]=max(dp[j],dp[j-v1[i]]+w1[i]);
		}
	}
	cout<<dp[v];
	return 0; 
}
  • 01 背包:j 逆序 j=m; j>=v[i](防止重复选)
  • 完全背包:j 正序 j=v[i]; j<=m(允许重复选取)

2、不考虑价值,恰好装满的方案数

  • 内层循环:j 从 v[i]正序到V
  • 转移:dp[j] = (dp[j] + dp[j‑v[i]]) % MOD
  • ①恰好装满 V:答案dp[V]
  • ②体积不超过 V:答案sum(dp[0]~dp[V])%MOD

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
int dp[1003];
int v1[1003],w1[1003];
int main(){
	int n,v;
	cin>>n>>v;
	dp[0]=1;
	for(int i=0;i<n;i++){
		cin>>v1[i]>>w1[i];
	}
	for(int i=0;i<n;i++){
		for(int j=v1[i];j<=v;j++){
			dp[j]+=dp[j-v1[i]];
		}
	}
	cout<<dp[v];
	return 0;
}

3、体积不超过 V,求能拿到最大价值的方案数

  • dp[j]:背包容量j,可以得到的最大价值,全局数组默认初始为 0
  • cnt[j]:背包容量j,取得dp[j]最大价值的方案数量

初始化cnt[0]=1;

  • cnt[0]=1:背包体积为 0,什么物品都不选,价值 0,算作 1 种合法方案。
  • cnt[1...v]全局默认 0,不需要赋值。

循环

复制代码
for(int i=0;i<n;i++)
    for(int j=v1[i];j<=v;j++)
  • j 从小到大正序:完全背包,允许同一物品重复选取。
  • 如果改成 01 背包,内层改为for(int j=v;j>=v1[i];j--)倒序,转移代码完全不变

状态转移

复制代码
int choose = dp[j-v1[i]] + w1[i]; //选当前物品的总价值
if(dp[j] < choose){
    //选物品价值更大,更新价值;方案继承j‑v1[i]的方案
	dp[j]=choose;
	cnt[j]=cnt[j-v1[i]]%N;
}else if(dp[j]==choose){
    //选、不选价值相等,两类方案相加
	cnt[j]=(cnt[j]+cnt[j-v1[i]])%N;
}
//choose < dp[j]:不选该物品,dp、cnt保持原样

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
int const N=1e9+7;
int dp[1003];
int cnt[1003];
int v1[1003],w1[1003];
int main(){
	int n,v;
	cin>>n>>v;
	for(int i=0;i<n;i++){
		cin>>v1[i]>>w1[i];
	}
	cnt[0]=1;
	for(int i=0;i<n;i++){
		for(int j=v1[i];j<=v;j++){
			if(dp[j]<dp[j-v1[i]]+w1[i]){
				dp[j]=dp[j-v1[i]]+w1[i];
				cnt[j]=cnt[j-v1[i]]%N;
			}
			else if(dp[j]==dp[j-v1[i]]+w1[i]){
				cnt[j]=(cnt[j]+cnt[j-v1[i]])%N;
			}
		}
	}
	cout<<cnt[v];
	return 0;
}

三、分组背包

一维DP

物品分成 n 组,每组最多只能选 1 件物品,组内物品互斥;求背包容量 v 下最大总价值。

数组含义

  • s[i]:第i组有多少件物品
  • v1[i][k],w1[i][k]:第i组,组内第k件物品的体积、价值
  • dp[j]:背包容量j,可以获得的最大价值

三层循环顺序(分组背包固定顺序)

复制代码
for(组 i : 0~n‑1)          //第一层:遍历组别
    for(j = v downto 0)   //第二层:容量倒序!不能正序
        for(组内物品k)    //第三层:遍历本组所有物品

循环顺序绝对不能乱

每一层作用

第一层 i:枚举每一组 一组一组处理,每组最多挑一件。

第二层 j:j 从 v 倒序到 0 * 和 01 背包一样倒序,防止同一组选多个物品

如果写成正序,就会变成完全背包效果,同一组可以重复拿多件,直接 WA。

第三层 k:遍历本组里面每一个物品 尝试:本组选第 k 个物品,更新 dp j。 转移式子:

复制代码
dp[j]=max(dp[j], dp[j‑v1[i][k]] + w1[i][k]);

dp[j‑v1[i][k]]是处理前面 i‑1 组得到的旧状态,保证本组最多只选一件。

代码模板

复制代码
#include<bits/stdc++.h>
using namespace std;
int s[108];
int v1[108][108];
int w1[108][108];
int dp[108];
int main(){
	int n,v;
	cin>>n>>v;
	for(int i=0;i<n;i++){
		cin>>s[i];
		for(int j=0;j<s[i];j++){
			cin>>v1[i][j]>>w1[i][j];
		}
	}
	for(int i=0;i<n;i++){
		for(int j=v;j>=0;j--){
			for(int k=0;k<s[i];k++){
				if(j>=v1[i][k]){
					dp[j]=max(dp[j],dp[j-v1[i][k]]+w1[i][k]);
				}
			}
		}
	}
	cout<<dp[v];
	return 0;
}

四、多重背包I、II

朴素暴力版

  • dp[j]:背包容量为j时,能拿到的最大价值。全局数组默认初始为 0。

三层循环拆解

复制代码
for(int i=0;i<n;i++)          //第一层:枚举每一类物品
{
    int s,v1,w1;
	cin>>v1>>w1>>s;
	for(int j=v;j>=v1;j--)    //第二层:容量j倒序(模仿01背包)
	{
        //k:枚举该物品选k件,k∈[1,s]
		for(int k=1;k<=s&&k*v1<=j;k++) 
		{
			dp[j]=max(dp[j], dp[j‑k*v1] + k*w1);
		}
	}
}

每一层作用

  1. 第一层 i:遍历每一类物品 每一类最多拿s个。
  2. 第二层 j:j 从 v 往下倒序 和 01 背包一样倒序,保证使用上一轮 i‑1 的旧 dp 状态,不会出现物品无限选。
  3. 第三层 k:暴力枚举选多少件 k代表当前物品选k件。 条件:
  • k<=s:不能超过该物品最多数量
  • k*v1 <= j:选 k 件的总体积不能超过当前背包容量 j

状态转移方程

复制代码
dp[j] = max(dp[j], dp[j - k*v1] + k*w1);

含义:选k件该物品,占用体积k*v1,获得价值k*w1;剩下容量j‑k*v1用前面物品的最优解。

复制代码
#include<bits/stdc++.h>
using namespace std;
int dp[109];
int main(){
	int n,v;
	cin>>n>>v;
	for(int i=0;i<n;i++){
		int s,v1,w1;
		cin>>v1>>w1>>s;
		for(int j=v;j>=v1;j--){
			for(int k=1;k<=s&&k*v1<=j;k++){
				dp[j]=max(dp[j],dp[j-k*v1]+k*w1);
			}
		}
	}
	cout<<dp[v];
	return 0;
}

二进制优化版

  • dp[j]:容量j背包的最大价值,全局数组默认初始 0。
  • vvvw:存放二进制拆分之后生成的虚拟物品的体积、价值。

把原来一类最多s件的物品,拆成若干个新物品,每个新物品代表一次性拿k件原物品。

二进制拆分核心原理

把数量s拆成若干个 2 的幂:(1,2,4,8...),再加上剩下余数。 0~s 之间任意数目,都可以用若干个拆分出的数字相加凑出来。

举例:s=13

  • k=1:取出 1,s=12,k=2
  • k=2:取出 2,s=10,k=4
  • k=4:取出 4,s=6,k=8
  • k=8 > 6,退出循环,把剩余 s=6 加入 拆分结果:1,2,4,61‑13任意数字都可以组合得到。

每一组k件,合并成一件虚拟物品:

  • 虚拟体积 = k ∗ v 1 k*v1 k∗v1
  • 虚拟价值 = k ∗ w 1 k*w1 k∗w1

第一段:读入物品 + 二进制拆分

复制代码
for(int i=0;i<n;i++){
	int v1,w1,s;
	cin>>v1>>w1>>s;
	int k=1;
	while(k<=s){
		vv.push_back(v1*k);
		vw.push_back(k*w1);
		s=s-k;
		k=k*2;
	}
	if(s>0){
		vv.push_back(v1*s);
		vw.push_back(s*w1);
	}
}

注意:这里直接修改局部变量s,不影响下一轮读入。

第二段:对所有虚拟物品跑 01 背包

复制代码
for(int i=0;i<vv.size();i++){
	for(int j=v;j>=vv[i];j--){
		dp[j]=max(dp[j],dp[j-vv[i]]+vw[i]);
	}
}

拆分完成后,每一个虚拟物品只能选一次 ,所以内层循环j倒序,标准 01 背包写法。

状态转移:

d p j = m a x ( d p j , d p j − v v \[ i ] + v w i ) dpj=max(dpj,dpj-vv\[i]+vwi) dpj=max(dpj,dpj−vv\[i]+vwi)

复制代码
#include<bits/stdc++.h>
using namespace std;
int dp[2003];
vector<int>vv;   //拆分后虚拟物品的体积
vector<int>vw;   //拆分后虚拟物品的价值
int main(){
	int n,v;
	cin>>n>>v;
	for(int i=0;i<n;i++){
		int v1,w1,s;
		cin>>v1>>w1>>s;
		int k=1;
		//二进制拆分:1,2,4,8......
		while(k<=s){
			vv.push_back(v1*k);
			vw.push_back(k*w1);
			s=s-k;
			k=k*2;
		}
		//剩余不足2的幂次,单独作为一组
		if(s>0){
			vv.push_back(v1*s);
			vw.push_back(s*w1);
		}
	}
	//全部虚拟物品当作01背包处理,j倒序
	for(int i=0;i<vv.size();i++){
		for(int j=v;j>=vv[i];j--){
			dp[j]=max(dp[j],dp[j-vv[i]]+vw[i]);
		}
	}
	cout<<dp[v];
	return 0;
}

五、混合背包

一维DP

混合背包问题:同一个题目里面同时存在三种物品

  1. s[i] = -101 背包物品,最多选 1 次
  2. s[i] = 0完全背包物品,可以无限选
  3. s[i] > 0多重背包物品 ,最多选s[i]件,使用二进制拆分优化转成 01 背包
  • vv,ww:vector,存储二进制拆分之后生成的 "新虚拟物品" 的体积和价值

分模块知识点

  1. s [i]==-1 01 背包

    for(int j=v;j>=v1[i];j--){
    dp[j]=max(dp[j],dp[j-v1[i]]+w1[i]);
    }

内层循环容量倒序,保证每件物品只能选一次。

  1. s [i]==0 完全背包

    for(int j=v1[i];j<=v;j++){
    dp[j]=max(dp[j],dp[j-v1[i]]+w1[i]);
    }

内层循环容量正序,允许物品多次选取。

  1. s [i]>0 多重背包(二进制拆分优化)

核心思想:把s件物品,拆成若干组(1,2,4,8...剩余数),每组当成一件新的 01 物品。 任意0~s之间的数量,都可以用这些组组合凑出来。

复制代码
vv.clear();
ww.clear();
int k=1;
while(k<=s[i]){
	vv.push_back(k*v1[i]);   //k件合并为1个虚拟物品
	ww.push_back(k*w1[i]);
	s[i]=s[i]-k;
	k=2*k;
}
if(s[i]>0){ //剩下不足2的幂次的部分单独作为一组
	vv.push_back(s[i]*v1[i]);
	ww.push_back(s[i]*w1[i]);
}
//拆分完成,全部当做01背包,倒序遍历
for(int jj=0;jj<vv.size();jj++){
	for(int kk=v;kk>=vv[jj];kk--){
		dp[kk]=max(dp[kk],dp[kk-vv[jj]]+ww[jj]);
	}
}

举例:s=13 拆分:1,2,4,61+2+4+6=13。 1‑13 任意数字都可以由其中若干项相加得到。

整体逻辑(混合背包)

遍历每一件物品,根据s[i]的标记,走不同背包逻辑:

  • 01 → j 倒序
  • 完全 → j 正序
  • 多重 → 二进制拆分,转为多件 01 物品,j 倒序

关键点:dp 数组是共用的,三种物品依次处理,状态不断滚动更新

相关推荐
fpcc1 小时前
跟我学C++中级篇——编译期的条件选择
开发语言·c++
兴通物联科技2 小时前
SMT PCB 微小 DataMatrix 码扫不动问题分析 兴通 XT8601B 600 万像素工业读码器落地实践
大数据·人工智能·单片机·嵌入式硬件·算法·计算机视觉
月华路5 小时前
G1 GC 对数组与大对象(Humongous)的处理
java·jvm·算法
我想走路带风5 小时前
LRU和最长前缀和(计算机网络算法)
计算机网络·算法
M78佐菲5 小时前
Linux学习笔记:进程
linux·笔记·学习·算法
ShineWinsu6 小时前
对于C++:C++20中线程、初始化、Lambda与内存视图等特性的解析
c++·c++20
徐小夕6 小时前
表格、文档、甘特、大屏、表单一站打通:pxcharts超级表格4.0正式上线!
前端·算法·github
张张的快乐时光呀!7 小时前
记录使用 MFC 打开文件对话框读取一张图片并做简单的灰度转换算法简单学习过程
c++·mfc