##[$AcWing$ $1013$. 机器分配](https://www.acwing.com/problem/content/1015/) ### 一、题目描述 总公司拥有 $M$ 台 **相同** 的高效设备,准备分给下属的 $N$ 个分公司。 各分公司若获得这些设备,可以为国家提供一定的盈利。盈利与分配的设备数量有关。 问:如何分配这$M$台设备才能使国家得到的盈利最大? 求出最大盈利值。 **分配原则**:每个公司有权获得任意数目的设备,但总台数不超过设备数 $M$。 **输入格式** 第一行有两个数,第一个数是分公司数 $N$,第二个数是设备台数 $M$; 接下来是一个 $N×M$的矩阵,矩阵中的第 $i$ 行第 $j$ 列的整数表示第 $i$ 个公司分配 $j$ 台机器时的盈利。 **输出格式** 第一行输出最大盈利值; 接下 $N$行,每行有 $2$ 个数,即分公司编号和该分公司获得设备台数。 答案不唯一,输出任意合法方案即可。 **数据范围** $1≤N≤10,1≤M≤15$ **输入样例**: ```cpp {.line-numbers} 3 3 30 40 50 20 30 50 20 25 30 ``` **输出样例**: ```cpp {.line-numbers} 70 1 1 2 1 3 1 ``` ### 二、题意理解 **样例解读**: ```cpp {.line-numbers} 3 3 30 40 50 20 30 50 20 25 30 ``` $3$个公司,$3$台机器,**机器都是一样的,一样的,记住,一样的**,要不题意理解不明白~ - $1$号公司 - 得到$1$台机器,$30$元 - 得到$2$台机器,$40$元 - 得到$3$台机器,$50$元 - $2$号公司 - 得到$1$台机器,$20$元 - 得到$2$台机器,$30$元 - 得到$3$台机器,$50$元 - $3$号公司 - 得到$1$台机器,$20$元 - 得到$2$台机器,$25$元 - 得到$3$台机器,$30$元 问,怎么分,使得国家的收益最大? **答**:$1$号公司得到$1$台机器,$2$号公司得到$1$台机器,$3$号公司得到$1$台机器,就是$30+20+20=70$,此时国家利益最大。 ### 二、分组背包 **套模型**:转换成 **分组背包问题** ,做 **等价变换** ① 第$i$个公司就是第$i$个分组 ② 每个分组中可以一台也不要$f[i][j]=f[i-1][j]$,如果背包能装的下的前提下: - 要一台(类比为第$1$个) - 要两台(类比为第$2$个) - ... - 要$S_i$台(类比为第$S_i$个) 本题特殊的地方就是需要求 **最优解时的路径** ,而且可能要的是 **字典序最小** 的路径。 ### 三、不同$OJ$此题的差别 两者差别: * $AcWing$:答案不唯一,输出任意合法方案即可($Special$ $Judge$) * 洛谷: $P.S.$要求答案的字典序最小 尽管本题是多阶段决策的最小字典序最优方案,但是背包都也类似。 下面是卡最小字典序的数据: ```cpp {.line-numbers} input 2 15 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 output 2 1 0 2 15 ``` **解决办法** * ① **$dfs$暴搜法** $dfs$保证找到最大答案的时候就是字典序最少的,因为我从$1 \sim n$号枚举用的多少机器,用的机器数量也是由少到多。当最后得到答案相等的情况下就不用需要比较字典序了,直接$return$,只有碰到大小不一的时候才更新答案机器数。 * ② **$dp$+记录路径法** * 如果$val > f[i][j]$转移,打印出来的路径是字典序最大的 * 如果$val \geq f[i][j]$转移,打印出来的路径是字典序最小的 >**$Q$:为什么加上等号就是按字典序最小输出呢?** ```cpp {.line-numbers} for (int k = 1; k <= j; k++) { int val = f[i - 1][j - k] + w[i][k]; if (val >= f[i][j]) { f[i][j] = val; path[i][j] = k; } } ``` **答**: 观察`f[i-1][j-k]`,计算式是$j-k$,$k$前面的符号是负号,也就是$k$越小,$j-k$越大;$k$越大,值越小。按这个逻辑,随着$k$长大,就会枚举到更小的$j-k$。 如果没有等号,是$k$越来越大时,$j-k$越来越小,当价值一样时,越来越小的个数无法更新结果,反之,如果有等号,就是获取到字典序。 **二维数组写法** ```cpp {.line-numbers} #include using namespace std; const int N = 30; int n, m; int w[N][N]; int f[N][N]; int path[N][N]; //致敬墨染空大神 void out(int i, int j) { if (i == 0) return; //走出界就完事了 int k = path[i][j]; out(i - 1, j - k); //利用递推的栈机制,后序输出,太强了~ printf("%d %d\n", i, k); } int main() { scanf("%d %d", &n, &m); for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) scanf("%d", &w[i][j]); /*1、原始版本*/ for (int i = 1; i <= n; i++) for (int j = 0; j <= m; j++) { f[i][j] = f[i - 1][j]; path[i][j] = 0; for (int k = 1; k <= j; k++) { int val = f[i - 1][j - k] + w[i][k]; if (val >= f[i][j]) { f[i][j] = val; path[i][j] = k; } } } /*2、优化一下代码*/ /* for (int i = 1; i <= n; i++) for (int j = 0; j <= m; j++) { for (int k = 0; k <= j; k++) { int val = f[i - 1][j - k] + w[i][k]; if (val >= f[i][j]) { f[i][j] = val; path[i][j] = k; } } }*/ //输出最大值 printf("%d\n", f[n][m]); //输出路径 out(n, m); return 0; } ``` **一维数组写法** ```cpp {.line-numbers} #include using namespace std; const int N = 20; int f[N]; int w[N]; int path[N][N]; //致敬墨染空大神 void out(int i, int j) { if (i == 0) return; //走出界就完事了 int k = path[i][j]; out(i - 1, j - k); //利用递推的栈机制,后序输出,太强了~ printf("%d %d\n", i, k); } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) scanf("%d", &w[j]); for (int j = m; j; j--) for (int k = 1; k <= j; k++) { int val = f[j - k] + w[k]; if (val >= f[j]) { f[j] = val; //在状态转移时,记录路径 path[i][j] = k; } } } //输出结果 printf("%d\n", f[m]); //输出路径 out(n, m); return 0; } ``` **找最短路的递归写法** ```cpp {.line-numbers} #include using namespace std; const int N = 20; int n, m; int w[N][N]; int f[N][N]; int a[N], al; void dfs(int u, int v) { if (u == 0) return; // 寻找当前状态f[i][j]是从上述哪一个f[i-1][k]状态转移过来的 for (int i = 0; i <= v; i++) { if (f[u - 1][v - i] + w[u][i] == f[u][v]) { a[++al] = i; dfs(u - 1, v - i); return; } } } int main() { // input cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> w[i][j]; // dp for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k <= j; k++) f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]); cout << f[n][m] << endl; // find path dfs(n, m); int id = 1; for (int i = al; i; i--) cout << id++ << " " << a[i] << endl; return 0; } ``` ### 四、深度优先搜索 数据范围比较小,$1 \leq N \leq 10,1 \leq M \leq 15$,把$m$个机器分配给$n$个公司,暴力遍历所有方案 记录分配方案,如果能更新最优解,顺便更新一下最优解的分配方案 ```cpp {.line-numbers} #include using namespace std; const int N = 11; const int M = 16; int n; int m; int path[N], res[N]; int w[N][M]; int mx; // u:第几个公司 s:已经产生的价值 r:剩余的机器数量 void dfs(int u, int s, int r) { if (u == n + 1) { if (s > mx) { mx = s; memcpy(res, path, sizeof path); } return; } for (int i = 0; i <= r; i++) { path[u] = i; dfs(u + 1, s + w[u][i], r - i); // 给u号公司分配i个机器 path[u] = 0; } } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> w[i][j]; dfs(1, 0, m); printf("%d\n", mx); // 输出最优答案时的路径 for (int i = 1; i <= n; i++) printf("%d %d\n", i, res[i]); return 0; } ```