背包问题
01背包问题
因为每件物品只有选与不选两种状态,所以该问题又称01背包问题。
AcWing 2. 01背包问题
f[i][j]:表示所有选法集合中,只从前i个物品中选,并且总体积≤j的选法的集合,它的值是这个集合中每一个选法的最大值.
状态转移方程:f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])
f[i-1][j]:不选第i个物品的集合中的最大值
f[i-1][j-v[i]]+w[i]:选第i个物品的集合,但是直接求不容易求所在集合的属性,这里迂回打击一下,先将第i个物品的体积减去,求剩下集合中选法的最大值.
import java.util.Scanner;
class Main{
static int M = 1010;
static int v[] = new int[M]; // 体积
static int w[] = new int[M]; // 价值
static int f[][] = new int[M][M]; // f(i,j)表示前i件物品中,体积不超过j的最大价值
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int N = reader.nextInt(), V = reader.nextInt();
for(int i = 1; i <= N; i ++){
v[i] = reader.nextInt();
w[i] = reader.nextInt();
}
for(int i = 1; i <= N; i ++){
for(int j = 1; j <= V; j ++){
// 第i件物品选不选
f[i][j] = f[i - 1][j]; // 不选第i件物品
if(v[i]<= j){ // 选第i件物品的前提是:放得下!
// 更新最大价值
f[i][j] = Math.max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
}
System.out.println(f[N][V]);
}
}
暴力搜索, 过一半样例
import java.util.Scanner;
class Main{
static int M = 1010;
static int N, V;
static boolean st[] = new boolean[M]; // 每件物品只能被使用一次
static int v[] = new int[M]; // 体积
static int w[] = new int[M]; // 价值
static long ans = 0;
public static void dfs(int u, int sum, int capacity){
if(capacity < 0){
return;
}
if(u > N){
if(sum > ans){
ans = sum;
}
return;
}
// 选第u个物品
if(!st[u] && capacity - v[u] >= 0){
st[u] = true;
dfs(u + 1, sum + w[u], capacity - v[u]);
st[u] = false;
}
// 不选第u个物品
dfs(u + 1, sum, capacity);
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
N = reader.nextInt();
V = reader.nextInt();
for(int i = 1; i <= N; i ++){
v[i] = reader.nextInt();
w[i] = reader.nextInt();
}
dfs(1, 0, V);
System.out.println(ans);
}
}
278. 数字组合
可以转化为01背包问题求方案数:
将总和 M看作背包容量;
将每个数
A
i
A_i
Ai 看作体积为
A
i
A_i
Ai 的物品;
import java.util.Scanner;
class Main{
static int M = 10010;
static int f[][] = new int[110][M]; // f(i,j)表示只考虑前i个数中,其和为j的选法的集合
static int a[] = new int[110];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt(), m = reader.nextInt();
for(int i = 1; i <= n; i ++){
a[i] = reader.nextInt();
}
f[0][0] = 1; // 注意初始化条件
for(int i = 1; i <= n; i ++){
f[i][0] = 1;
for(int j = 1; j <= m; j ++){
f[i][j] = f[i - 1][j]; // 不选第i个数
if(a[i] <= j){ // 选第i个数
f[i][j] += f[i - 1][j - a[i]];
}
}
}
System.out.println(f[n][m]);
}
}
从大体上来看,其实就是从 n 个人中选 m 人,使得辩方总分和控方总分之差的绝对值最小。
这么看来,本题是一个 01 背包问题,我们将 n 个人看作 n 个物品,那么每个物品会有三个体积:
1. 人数,每个候选人都是 1 个人,最终要选出 m 个人
2. 辩方得分,即辩方给每个候选人打的分数 a[i]
3. 控方得分,即控方给每个候选人打的分数 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] = true,目标是 f[m][d][p] = true,要求 |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] 尽量大。
作者:小小_88
链接:https://www.acwing.com/solution/content/128611/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1234. 倍数问题
x + a i x + a_i x+ai 与 k − a i k - a_i k−ai在modk下同余。
前i个数选0个模k的最大值都为0,其他都应该是负无穷,因为选0个数总和只能为0。而没选物品的总和为0 %k不可能为其他值。
1234. 倍数问题
【题目描述】
暴力枚举 迭代写法
【思路】
分别枚举m、n的个数
for(m个数)
for(n个数)
k = m * m个数 + n * n个数
O(N^2),只能枚举答案在1000以内的
import java.util.Scanner;
public class Main{
static int N = 10010;
static int []f = new int [N];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt(), m = reader.nextInt();
for(int i = 0; i < N / m + 1; i ++)//枚举m的个数
for(int j = 0; j < N / n + 1; j++)//枚举n的个数
{
int k = m * i + n * j;
if( k >= N ) continue;
f[k] = 1;
}
for(int i = N - 1; i >= 0;i --)
if(f[i] == 0){
System.out.println(i);
break;
}
}
}
暴力枚举——递归写法
过5/10数据
import java.util.Scanner;
public class Main{
//判断x能否由m、n组成
public static boolean dfs(int x, int m, int n){
if( x < 0) return false;
//x == 0 说明x能由若干m、n组成
if( x == 0 ) return true;
if( x >= m && dfs(x - m, m, n)) return true;
if( x >= n && dfs(x - n, m, n)) return true;
return false;
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int m = reader.nextInt(), n = reader.nextInt();
for(int i = 1000; i >= 1; i --)
{
if(!dfs(i, m, n)){
System.out.println(i);
break;
}
}
}
}
错误写法:
对于有返回值的递归函数,要注意对中间结果的保存和处理,只是把它扔掉了
import java.util.Scanner;
public class Main{
//判断x能否由m、n组成
public static boolean dfs(int x, int m, int n){
if( x < 0) return false;
//x == 0 说明x能由若干m、n组成
if( x == 0 ) return true;
if( x >= m ) return dfs(x - m, m, n);
if( x >= n ) return dfs(x - n, m, n);
return false;
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int m = reader.nextInt(), n = reader.nextInt();
for(int i = 1000; i >= 1; i --)
{
if(!dfs(i, m, n)){
System.out.println(i);
break;
}
}
}
}
AC代码
引理
给定a,b,如果d =gcd(a, b) >1,那么其凡不是d倍数的a,b都凑不出,因此其凑不出来的数目是INF个。
结论
若a、b均为正整数且gcd(a,b) = 1, 那么 ax + by ,x >= 0, y >= 0,最大不能表示的数为 (a - 1) * (b - 1) = a*b - a - b
import java.util.Scanner;
public class Main{
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int a = reader.nextInt(), b = reader.nextInt();
//公式: (a - 1) * (b - 1) - 1 =ab -a - b
System.out.println(a * b - a - b);
}
}
质数拆分
f[i][j] = Math.max(f[i - 1][j - prime[i]] + 1, f[i][j]);
注意这里求的是方案数目
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
static int N = 2022;
static int [] prime = new int[N];
static boolean st[] = new boolean[N + 1];
static int cnt = 0;
static void getPrime2(int x){
// 埃氏筛法
for(int i = 2; i <= x; i ++){
if(!st[i]){
prime[++cnt] = i;
// 质数的所有倍数
for(int j = i + i; j <= x; j += i){
st[j] = true;
}
}
}
}
static void getPrime(int x){
// 埃氏筛法
for(int i = 2; i <= x; i ++){
if(!st[i]){
prime[++cnt] = i;
}
for(int j = i + i; j <= x; j += i){
st[j] = true;
}
}
}
public static void main(String[] args) {
getPrime2(N);
// for(int i = 1; i <= cnt; i ++){
// System.out.println(prime[i]);
// }
int [][]f = new int[cnt + 1][N + 1];
// f[0][0] = 1;
for(int i = 1; i <= cnt; i ++){
for(int j = 0; j <= N; j ++ ){
f[i][j] = f[i - 1][j]; // 不使用第i个数字
if(j >= prime[i]){
f[i][j] = Math.max(f[i - 1][j - prime[i]] + 1, f[i][j]);
}
}
}
System.out.println(f[cnt][N]);
}
}
质数分解
问题描述
将2019拆分为若干个两两不同的质数之和,一共有多少种不同的方法?
注意交换顺序视为同一种方法,例如2 + 2017 = 2019与
2017 +2 = 2019视为同一种方法。
【Attention】
若干个两两不同的质数之和,不止两个哦。
枚举 应当是枚举2019以内,而不是sqrt(2019)内的质数。
数的范围从2~2018,且必须是质数。两两不同,即不重复,每个质数只能最多被用一次。明显01背包。
f(i,j)表示只考虑前i个质数,和为j的选法集合,属性为方案个数。
所求即为f(K, 2019)
01背包
import java.util.Scanner;
public class Main {
static int N = 2019;
static long f[][] = new long[N][N + 5];
static int a[] = new int[N];
public static boolean check(int x){
for(int i = 2; i * i <= x; i ++){
if(x % i == 0){
return false;
}
}
return true;
}
public static void main(String[] args) {
int K = 0; // 注意如果K是从1开始的,所以质数的个数为K-1,不是307,而是306
// 枚举 2~2018的质数,
for(int i = 2; i < 2019; i ++){
if(check(i)){
a[++K] = i;
}
}
f[0][0] = 1;
for(int i = 1; i <= K; i ++){
for(int j = 0; j <= N; j ++){
f[i][j] = f[i - 1][j];
if(j >= a[i]){
f[i][j] += f[i - 1][j - a[i]];
}
}
}
System.out.println(f[K][2019]);
}
}
爆搜
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
static int N = 2019;
static int K, ans;
static int f[] = new int[N];
static boolean vis[] = new boolean[N];
public static boolean check(int x){
for(int i = 2; i * i <= x; i ++){
if(x % i == 0){
return false;
}
}
return true;
}
public static void dfs(int u, int p, int sum){
if(sum > 2019){
return;
}
if(sum == 2019){
ans ++;
return;
}
for(int i = p; i < K; i ++){ // p保证递增选择, vis保证两个数不同
if(!vis[i]){
vis[i] = true;
dfs(u + 1, i + 1, sum + f[i]);
vis[i] = false;
}
}
}
// 直接查看另一半是否在数组中即可
public static void main(String[] args) {
// 枚举 2~2018的质数,
for(int i = 2; i < 2019; i ++){
if(check(i)){
f[K++] = i;
}
}
//System.out.println(K); // 306
dfs(0, 0, 0);
System.out.println(ans);
}
}
注意这里求的是定义的集合内的元素的最大个数
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
static int N = 2022;
static int [] prime = new int[N];
static boolean st[] = new boolean[N + 1];
static int cnt = 0;
static void getPrime2(int x){
// 埃氏筛法
for(int i = 2; i <= x; i ++){
if(!st[i]){
prime[++cnt] = i;
// 质数的所有倍数
for(int j = i + i; j <= x; j += i){
st[j] = true;
}
}
}
}
static void getPrime(int x){
// 埃氏筛法
for(int i = 2; i <= x; i ++){
if(!st[i]){
prime[++cnt] = i;
}
for(int j = i + i; j <= x; j += i){
st[j] = true;
}
}
}
public static void main(String[] args) {
getPrime2(N);
// for(int i = 1; i <= cnt; i ++){
// System.out.println(prime[i]);
// }
int [][]f = new int[cnt + 1][N + 1];
// f[0][0] = 1;
for(int i = 1; i <= cnt; i ++){
for(int j = 0; j <= N; j ++ ){
f[i][j] = f[i - 1][j]; // 不使用第i个数字
if(j >= prime[i]){
f[i][j] = Math.max(f[i - 1][j - prime[i]] + 1, f[i][j]);
}
}
}
System.out.println(f[cnt][N]);
}
}
完全背包问题
模板题——3. 完全背包问题
import java.util.Scanner;
class Main{
static int N = 1010;
static int v[] = new int[N];
static int w[] = new int[N];
static int f[][] = new int[N][N]; // 第一维表示物品,第二维表示体积,其值为价值
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt(), t = reader.nextInt();
for(int i = 1; i <= n; i++) {
v[i] = reader.nextInt();
w[i] = reader.nextInt();
}
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= t; j++){
f[i][j] = f[i - 1][j]; // 不放
for(int k = 0; k * v[i] <= j;k ++) { // 放
f[i][j] = Math.max(f[i][j], f[i - 1][j - k * v[i]]+ k * w[i]);
}
}
}
System.out.println(f[n][t]);
}
}
k是枚举的,k的值不能无限大,因为物品不能超过背包的容量
程序中的f[i][j]本来应该是
f[i][j]=f[i-1][j]; //注意如果有这句话应该是放在第二三层for循环之间的!
f[i][j]=Math.max(f[i][j], f[i-1][j-k*v[i]]+w[i]*k); //这句在第三层for循环内部
由于k是从0开始循环的,所以f[i][j]可以简写成
f[i][j]=Math.max(f[i][j], f[i-1][j-k*v[i]]+w[i]*k);
上述代码还可以优化为O(n*m)如下:
import java.util.Scanner;
class Main{
static int N = 1010;
static int v[] = new int[N];
static int w[] = new int[N];
static int f[][] = new int[N][N]; // 第一维表示物品,第二维表示体积,其值为价值
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt(), k = reader.nextInt();
for(int i = 1; i <= n; i++) {
v[i] = reader.nextInt();
w[i] = reader.nextInt();
}
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= k; j++){
f[i][j] = f[i - 1][j]; // 不放:划分第一个不包含i的方案
// 划分的包含1-k个i的方案,因为可能不存在,所以需要判断一下,v[i]的大小是不是超过总容量j
if(j >= v[i]){
// 注意是 f[i][j - v[i]] + w[i]而不是f[i - 1][j - v[i]] + w[i]
f[i][j] = Math.max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
}
System.out.println(f[n][k]);
}
}
包子凑数
import java.util.Scanner;
class Main{
static int N = 110, M = 10000;
static int a[] = new int [N];
static boolean f[][] = new boolean[N][M];
static int gcd(int a, int b){
if(b == 0) {return a;}
return gcd(b, a % b);
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
for(int i = 1; i <= n; i ++) {
a[i] = reader.nextInt();
}
int d = 0;
for(int i = 1; i <= n; i ++){
d = gcd(d, a[i]);
}
if(d != 1) {
System.out.println("INF");
}else{
// 0个物品,重量为0,合法方案
f[0][0] = true;
for(int i = 1; i <= n; i ++){
for(int j = 0; j < M; j ++){
f[i][j] = f[i - 1][j];
for(int k = 0; k * a[i] <= j; k ++){
f[i][j] |= f[i - 1][j - k * a[i]];
}
}
}
int res = 0;
for(int j = 1; j < M; j ++){
if(!f[n][j]){
res ++;
// System.out.println(j);
}
}
System.out.println(res);
}
}
}
import java.util.Scanner;
class Main{
static int N = 10010;
static int a[] = new int[110];
static boolean f[][] = new boolean [110][N];
public static int gcd(int a, int b){
if( b == 0) return a;
return gcd( b, a % b );
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
for(int i = 1; i <= n; i ++) a[i] = reader.nextInt();
int d = 0; // 公约数
for(int i = 1; i <= n; i ++)
d = gcd(a[i], d); //gcd(2,0) = 2
if( d != 1) System.out.println("INF");
else{
f[0][0] = true;
for(int i = 1; i <= n; i ++)
for(int j = 0; j < N; j ++){
f[i][j] |= f[i - 1][j];
if( j >= a[i]) f[i][j] |= f[i][j - a[i]];
}
int res = 0;
for(int i = 1; i < N; i ++)
if( ! f[n][i] ) res ++;
System.out.println(res);
}
}
}
AcWing 3382. 整数拆分(每日一题)
import java.util.Scanner;
class Main{
static int N = 1000010;
static int k = 21; // 2^0 ~ 2^20
static int a[] = new int [N];
static int f[][] = new int[21][N];
static int mod = (int)1e9;
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
a[1] = 1;
//
// int k = (int)(Math.log(n + 1) / Math.log(2)); erro: 2次幂没做限制
for(int i = 2; i <= k; i ++) {
a[i] = a[i - 1] << 1 ;
}
// 完全背包问题,次幂可以使用无数次
for(int i = 0; i < k; i ++) {
f[i][0] = 1; // 初始化,什么都不选是一种选法
}
for(int i = 1; i < k; i ++){
for(int j = 1; j <= n; j ++){
f[i][j] = (f[i][j] + f[i - 1][j]) % mod;
if(a[i] <= j){
f[i][j] = (f[i][j] + f[i][j - a[i]]) % mod;
}
}
}
System.out.println(f[20][n]);
}
}
2022
数字即为物品,每个数字的编号相当于重量,每个数字的体积为1。问题等价于从前2022个物品钟选出10个满足重量刚好为2022。
用三维数组f(i,j,k)表示从前i个物品中选出j个,其重量和为k的选法的集合,其属性为选法个数。则所求为f[2022][10][2022]
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
static int N = 2025;
static long f[][][] = new long[N][15][N];
public static void main(String[] args) {
for(int i = 0; i < N; i ++) { // 容量为0(选0个物品)重量为0的方案数为1
f[i][0][0] = 1;
}
int n = 2022;
for(int i = 1; i <= n; i ++){ // 物品数
for(int j = 1; j <= 10; j ++){ // 容量(选择的数字个数)
for(int k = 1; k <= n; k ++){ // 重量
f[i][j][k] = f[i - 1][j][k]; // 不选第i个数
if(k >= i){ // 选择第i个数
//f[i][j][k] = Math.max(f[i - 1][j - 1][k - i] + 1, f[i][j][k]);
f[i][j][k] += f[i - 1][j - 1][k - i];
}
}
}
}
System.out.println(f[2022][10][2022]);
}
}
自然数拆分
本题要求用若干个数相加为 n,求方案数。
若干个正整数就是若干个物品,要想能相加为 n,且至少两个数相加,能用上的数就是 1 ~ n - 1,共 n - 1 个物品,价值就是数值本身。
每个物品能重复使用,因此本题是经典的完全背包,直接套模板
/*
完全背包问题
集合表示: f(i,j)表示前i个数和为j的方案的集合
属性: 个数
状态计算:
f(i,j) = f(i - 1, j)
f(i,j) += f(i-1, j - k * a[i]) , k = 1, 2, ……
*/
import java.util.Scanner;
public class Main{
static int N = 4010;
static int a[] = new int[N];
static long f[][] = new long [N][N];
static long MOD = 2147483648L;
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
for(int i = 1; i <= n; i ++){
a[i] = i;
}
f[0][0] = 1; // 只考虑前0个数,其和为0的方案数为1
for(int i = 1; i <= n; i ++){
for(int j = 0; j <= n; j ++){
f[i][j] = f[i - 1][j];
if(j >= a[i]){
f[i][j] += f[i][j - a[i]]; // 注意:完全背包是f[i][j - a[i]]而不是f[i - 1][j - a[i]]
}
f[i][j] %= MOD;
}
}
System.out.println(f[n - 1][n]%MOD); // 必须拆为2个数,所以没有n
}
}
数位dp
import java.util.Scanner;
class Main{
static int number[] = new int[15];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt(); // 1 ~ n 出现的1的个数 int 范围是 2 ^ 31 - 1
// 枚举 1所在的位置(个十百千万……位)
int len = 0;
int k = n;
// 按从低位到高位的顺序存储
while(k > 0){
number[len ++] = k % 10;
k /= 10;
}
long res = 0;
for(int i = len - 1; i >= 0; i --){
int high = 0, low = 0, t = 1;
for(int j = len - 1; j > i; j --){
high = high * 10 + number[j];
}
for(int j = i - 1; j >= 0; j --){
low = low * 10 + number[j];
t *= 10;
}
res += high * t;
if(number[i] == 1){
res += low + 1;
}
if(number[i] > 1){
res += t;
}
}
System.out.println(res);
}
}
二进制问题
import java.util.Scanner;
import java.util.Arrays;
public class Main {
static long n, K;
static long[][] dp = new long[66][66];
static int[] num = new int[66];
/*
len:表示当前正在处理的二进制数的位数,也就是二进制数的长度;
sum:表示当前二进制数中已经出现的 1 的个数;
limit:表示当前位是否有限制。如果为 true,则表示当前位上的数不能大于 num[len],否则可以随意取值。
*/
static long dfs(int len, int sum, boolean limit){
// 已经处理到了最后一位,则判断sum是否等于K,是则返回1,否则返回0
if(len == 0){
return sum == K ? 1: 0;
}
// 优化搜索:如果该状态已经被计算过且当前为没有限制
if(dp[len][sum] != -1 && !limit){
return dp[len][sum];
}
int up = limit ? num[len] : 1; // 该位能否取1
long res = 0;
// 枚举当前位的所有可能取值
for(int i = 0; i <= up; i ++){
res += dfs(len - 1, sum + (i == 1 ?1 : 0), limit && i == up);
}
return dp[len][sum] = res;
}
static long solve(long n) {
for (long[] longs : dp) Arrays.fill(longs, -1);
int len = 0;
// 分离位数:十进制转二进制,按低位到高位的顺序存储
while (n != 0) {
num[++len] = (int) (n & 1);
n >>= 1;
}
return dfs(len, 0, true);
}
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
n = cin.nextLong();
K = cin.nextLong();
System.out.println(solve(n));
}
}
## 异或三角形
[]()
```java
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
static int ans[] = new int[5];
static long res = 0;
// 求x以内能满足如下条件的三个数
public static void dfs(int u, int cur, int x){
//System.out.println(u + " "+ cur+" " + x);
if(u == 3){
if(ans[0] + ans[1] > ans[2]){
long k = ans[0] ^ ans[1];
if((k ^ ans[2]) == 0){
res ++;
}
}
return;
}
for(int i = cur; i <= x; i ++){
ans[u] = i;
dfs(u + 1, i, x);
}
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int t = scan.nextInt();
for(int i = 0; i < t; i ++){
int a = scan.nextInt();
long sum = 0;
res = 0;
for(int j = 1; j <= a; j ++){
dfs(0, 1, j);
sum += res;
}
System.out.println(sum * 6); // 六种排列
}
scan.close();
}
}
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
ios::sync_with_stdio(false);
int T;cin>>T;
while(T--)
{
ll n;cin>>n;
ll ans=0;
for(ll i=1;i<=n;i++)
{
for(ll j=1;j<=n;j++)
{
ll k=i^j; // 因为要求异或为0,故第三个数必为i与j异或的结果
if(k<=n&&k>=1&&i+j>k&&i+k>j&&k+j>i)ans++;
}
}
printf("%lld\n",ans);
}
return 0;
}
线性dp
费用问题
线性dp
/*
问题实际等价于从N个数中选择M个数字,要求这M个数字之和要最大。且选择的数字有条件限制,即满足步数限制(b[i])。
f(a, b, c, d) 集合:定义走到终点卡片1使用a张,卡片2使用了b张……的所有方案
属性:方案的最大值
格子, 卡片
i - 1(判空)
i - 2
i - 3
i - 4
i 1 2 3 …… i …… n
*/
import java.util.Scanner;
class Main{
static int N = 355, M= 125;
static int w[] = new int[N];
static int cnt[] = new int[5];
static int f[][][][] = new int[45][45][45][45];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt(), m = reader.nextInt();
for(int i = 1; i <= n; i ++){
w[i] = reader.nextInt();
}
for(int i = 1; i <= m; i ++){
cnt[reader.nextInt()] ++;
}
f[0][0][0][0] = w[1];
for(int a = 0; a <= cnt[1]; a ++){
for(int b = 0; b <= cnt[2]; b ++){
for(int c = 0; c <= cnt[3]; c ++){
for(int d = 0; d <= cnt[4]; d ++){
int t = 1 + a + b * 2 + c * 3 + d * 4; // 加上初始的一步
if(a > 0){
f[a][b][c][d] = Math.max(f[a][b][c][d], f[a - 1][b][c][d] + w[t]);
}
if(b > 0){
f[a][b][c][d] = Math.max(f[a][b][c][d], f[a][b - 1][c][d] + w[t]);
}
if(c > 0){
f[a][b][c][d] = Math.max(f[a][b][c][d], f[a][b][c - 1][d] + w[t]);
}
if(d > 0){
f[a][b][c][d] = Math.max(f[a][b][c][d], f[a][b][c][d - 1] + w[t]);
}
}
}
}
}
System.out.println(f[cnt[1]][cnt[2]][cnt[3]][cnt[4]]);
}
}
记忆化搜索
import java.util.Scanner;
public class Main {
static final int N = 350 + 1;
static final int M = 40 + 1;
static int n, m, bi;
static int[] a = new int[N];
static int[] b = new int[5];
static int[][][][] dp = new int[M][M][M][M];
static int recurMemorized(int k, int b1, int b2, int b3, int b4) {
if (dp[b1][b2][b3][b4] != 0) return dp[b1][b2][b3][b4];
int maxNext = 0;
if (b1 > 0) maxNext = Math.max(maxNext, recurMemorized(k + 1, b1 - 1, b2, b3, b4));
if (b2 > 0) maxNext = Math.max(maxNext, recurMemorized(k + 2, b1, b2 - 1, b3, b4));
if (b3 > 0) maxNext = Math.max(maxNext, recurMemorized(k + 3, b1, b2, b3 - 1, b4));
if (b4 > 0) maxNext = Math.max(maxNext, recurMemorized(k + 4, b1, b2, b3, b4 - 1));
return dp[b1][b2][b3][b4] = a[k] + maxNext;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
m = scanner.nextInt();
for (int i = 1; i <= n; ++i) a[i] = scanner.nextInt();
for (int i = 1; i <= m; ++i) {
bi = scanner.nextInt();
++b[bi];
}
System.out.println(recurMemorized(1, b[1], b[2], b[3], b[4]));
scanner.close();
}
}
1051. 最大的和
最大子段和
// 1 -1 2 2 3 -3 4 -4 5 -5
//0 1 0 2 4 7 4 8 4 9 4
int n = reader.nextInt();
for(int i = 1; i <= n; i ++){
a[i] = reader.nextInt();
}
int f[] = new int[N]; // 表示以第i个数字结尾的最大字段和
f[0] = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j ++){
f[i] = Math.max(a[i], a[i] + f[i - 1]);
}
}
AcWing 898. 数字三角形
【题目描述】
自底向上的走法
由于到顶部出口是唯一确定的
【思路】
for(i 从n - 2到 0)
for(j 从0到i)
f[i][j] = max(f[i + 1][j],f[i + 1][j + 1]) +f[i][j]
import java.util.Scanner;
import java.lang.Math;
class Main{
static int N = 510;
static int f[][] = new int[N][N];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
for(int i = 0; i < n; i ++)
for(int j = 0; j <= i; j ++)
f[i][j] = reader.nextInt();
for(int i = n - 2; i >= 0; i --)
for(int j = 0; j <= i; j ++){
f[i][j] = Math.max(f[i + 1][j],f[i + 1][j + 1]) +f[i][j];
}
System.out.println(f[0][0]);
}
}
自顶向下的写法
/**
自顶向下走:
由于当前格子只可能是从左上角或右上角走过来 路径是唯一确定的,只需选择较大者
便可确定一条从上走到当前位置的路径
底部有多个出口,那么走到底部就有多条路径
*/
import java.util.Scanner;
import java.lang.Math;
class Main{
static int N = 510;
static int f[][] = new int[N][N];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
//注意数据有负数 所以要初始化边界
for(int i = 0; i < N; i ++)
for(int j = 0; j < N; j ++)
f[i][j] = Integer.MIN_VALUE;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
f[i][j] = reader.nextInt();
for(int i = 2; i <= n; i ++){
for(int j = 1; j <= i; j ++){
f[i][j] += Math.max(f[i - 1][ j - 1] , f[i - 1][j]);
}
}
int res = Integer.MIN_VALUE;
for(int i = 1; i <= n; i ++)
if( f[n][i] > res ) res = f[n][i];
System.out.println(res);
}
}
注意 :
从底部往顶部的走法,由于到顶部出口是唯一确定的,所以即是最大值。
但是从顶部走到底部有多个出口,所以还有for循环取最大值。
相似题
Acwing 1015. 摘花生
AcWing 895. 最长上升子序列
【题目描述】
AcWing 895. 最长上升子序列
【思路】
图源 Acwing 小呆呆
状态计算,根据倒数第二个数可以是什么(a[1]、a[2] ……a[k]、……a[i - 1])将集合划分为i - 1个子集。在所有子集中取max然后加上最后一个是a[i]的1这个长度即为f[i]。
7
a[i]:3 1 2 1 8 5 6
f [i]:1 1 2 1 3 3 4
f[i]表示以第i个数字结尾的序列中的最长递增子序列
初始化f数组为全1
for(int i = 1; i < n; i ++)
for(int j = 0; j < i; j ++)
if( a[i] > a[j]) f[i] = Math.max(f[j] + 1, f[i]);
import java.util.Scanner;
import java.lang.Math;
class Main{
static int N = 1010;
static int f[] = new int[N];
static int a[] = new int[N];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
for(int i = 0; i < n; i ++ ) a[i] = reader.nextInt();
for(int i = 0; i < n; i ++ ) f[i] = 1;
for(int i = 1; i < n; i ++)
for(int j = 0; j < i; j ++)
if( a[i] > a[j]) f[i] = Math.max(f[j] + 1, f[i]);
int ans = 0;
for(int i = 0; i < n; i ++)
if( f[i] > ans) ans = f[i];
System.out.println(ans);
}
}
AcWing 896. 最长上升子序列 II
【题目描述】
【思路】
以3 1 2 1 8 5 6为例,
长度是1的子序列集和中有 {3},{1}, 但是可以发现{3}是多余的,因为1比3更小,使用范围更大,更优。
受此启发,可以以长度来划分集合,属性值保存:长度为len的子序列结尾的最小值。
根据上升子序列的特点可以知道,q[len]一定是严格递增的。
1、从前往后枚举数组a[i],二分查找出q[len]中最后一个小于 a[i]的位置mid。a[i]即要接在mid之后,以a[i]结尾的上升子序列长度为mid + 1。同时一定有q[mid + 1] <= a[i],更新q[mid + 1]为a[i].
import java.util.Scanner;
public class Main{
static int N = 100010;
static int a[] = new int[N];
static int q[] = new int[N];
public static void main(String args[]){
Scanner reader = new Scanner (System.in);
int n = reader.nextInt();
for(int i = 0; i < n; i ++) a[i] = reader.nextInt();
int len = 0;
q[0] = Integer.MIN_VALUE;
for(int i = 0; i < n; i ++)
{
//查找最后一个小于a[i]的位置mid
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;
}
//更新长度为l + 1 上升子序列的结尾最小值为a[i]
q[l + 1] = a[i];
//更新len
len = Math.max(len, l + 1);
}
System.out.println(len);
}
}
AcWing 897. 最长公共子序列
【题目描述】
【思路】
import java.util.Scanner;
public class Main{
static int N = 1010;
static int f[][] = new int[N][N];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int m = reader.nextInt(), n = reader.nextInt();
String A = reader.next(), B = reader.next();
for(int i = 1; i <= m; i ++)
for(int j = 1; j <= n; j ++)
{
f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
if( A.charAt(i - 1) == B.charAt(j - 1) )
f[i][j] = Math.max(f[i][j], f[i - 1][j - 1] + 1);
}
System.out.println(f[m][n]);
}
}
记忆化dp
AcWing 901. 滑雪
【题目描述】
dfs暴力搜
过8/11
import java.util.Scanner;
public class Main{
static int N = 310;
static int R, C;
static int f[][] = new int[N][N];
static boolean st[][] = new boolean[N][N];
static int ans = 0;
static int dir[][] = { {-1, 0}, {0, 1}, {1, 0}, {0, - 1}};
public static void dfs(int x, int y, int cur, int step){
if( step > ans) ans = step;
st[x][y] = true;
for(int i = 0; i < 4; i ++)
{
int a = x + dir[i][0], b = y +dir[i][1];
if( a < 0 || a >= R || b < 0 || b >= C) continue;
if( st[a][b] ) continue;
if( cur > f[a][b]){
dfs(a, b, f[a][b], step + 1);
}
}
st[x][y] = false;
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
R = reader.nextInt();
C = reader.nextInt();
for(int i = 0; i < R; i ++)
for(int j = 0; j < C; j ++)
f[i][j] = reader.nextInt();
//每一个起点
for(int i = 0; i < R; i ++)
for(int j = 0; j < C; j ++){
dfs(i, j, f[i][j], 1);
}
System.out.println(ans);
}
}
dp写法
【思路】
import java.util.Scanner;
import java.util.Arrays;
public class Main{
static int N = 310;
static int R, C;
static int h[][] = new int[N][N];
static int f[][] = new int[N][N];
static int dir[][] = { {-1, 0}, {0, 1}, {1, 0}, {0, - 1}};
public static int dfs(int x, int y){
if( f[x][y] != -1) return f[x][y];
//至少可以是该点自己
f[x][y] = 1;
for(int i = 0; i < 4; i ++)
{
int a = x + dir[i][0], b = y + dir[i][1];
//四种滑法取最大值
if( a >= 0 && a < R && b >= 0 && b < C && h[a][b] < h[x][y]){
f[x][y] = Math.max(f[x][y] ,dfs(a, b) + 1);
}
}
return f[x][y];
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
R = reader.nextInt();
C = reader.nextInt();
for(int i = 0; i < R; i ++)
for(int j = 0; j < C; j ++)
h[i][j] = reader.nextInt();
//每一个起点
for(int i = 0; i < R; i ++) Arrays.fill(f[i], - 1);
int ans = 0;
for(int i = 0; i < R; i ++)
for(int j = 0; j < C; j ++){
ans = Math.max( ans, dfs(i, j) );
}
System.out.println(ans);
}
}
备忘录写法【卡住了未完善】
import java.util.Scanner;
public class Main{
static int N = 310;
static int R, C;
static int f[][] = new int[N][N];
static boolean st[][] = new boolean[N][N];
static int ans = 0;
static int dir[][] = { {-1, 0}, {0, 1}, {1, 0}, {0, - 1}};
static int memo[][][][] = new int[N][N][N][N];//四维备忘录 [当前位置][下一个位置]
public static int dfs(int x, int y, int cur, int step){
if(){
}
st[x][y] = true;
for(int i = 0; i < 4; i ++)
{
int a = x + dir[i][0], b = y +dir[i][1];
if( a < 0 || a >= R || b < 0 || b >= C) continue;
if( st[a][b] ) continue;
if( cur > f[a][b]){
//搜索前先查找备忘录数据
if(memo[x][y][a][b] != 0) return memo[x][y][a][b];
else{
//查找后存储
memo[x][y][a][b] = dfs(a, b, f[a][b], step + 1);
}
}
}
st[x][y] = false;
return ;
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
R = reader.nextInt();
C = reader.nextInt();
for(int i = 0; i < R; i ++)
for(int j = 0; j < C; j ++)
f[i][j] = reader.nextInt();
//每一个起点
for(int i = 0; i < R; i ++)
for(int j = 0; j < C; j ++){
dfs(i, j, f[i][j], 1);
}
System.out.println(ans);
}
}
区间dp
AcWing 282. 石子合并
【题目描述】
【思路】
import java.util.Scanner;
public class Main{
static int N = 310;
static int s[] = new int[N]; //前缀和数组
static int f[][] = new int[N][N]; //状态数组
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
for(int i = 1; i <= n; i ++){
s[i] = reader.nextInt();
s[i] += s[i - 1];
}
//枚举区间长度
for(int len = 2; len <= n; len ++)
//枚举左端点 len = 2 i = n - 1
for(int i = 1; i + len - 1 <= n; i ++){
//枚举区间右端点
int j = i + len - 1;
f[i][j] = Integer.MAX_VALUE;
for(int k = i; k < j; k ++){
f[i][j] = Math.min( f[i][k] + f[k + 1][j], f[i][j]);
}
f[i][j] += s[j] - s[i - 1];
}
System.out.println(f[1][n]);
}
}
AcWing 3417. 砝码称重
import java.util.Scanner;
class Main{
static int N = 110, M = 200010;// 正整数和负整数重量
static int B = M /2;
static int w[] = new int[N];
static boolean f[][] = new boolean[N][M];
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
int m = 0;
for(int i = 1; i <= n; i ++){
w[i] = reader.nextInt();
m += w[i];
}
f[0][B] = true; // f[0][0] = true;
for(int i = 1; i <= n; i ++){
for(int j = - m; j <= m; j ++){
f[i][j + B] = f[i - 1][j + B]; // 不选第i个砝码
if(j - w[i] >= -m) {
f[i][j + B] |= f[i - 1][j - w[i] + B];
}
if(j + w[i] <= m) {
f[i][j + B] |= f[i - 1][j + w[i] + B];
}
}
}
int ans = 0;
for(int j = 1; j <= m; j ++){
if(f[n][j + B]){
ans ++;
}
}
System.out.println(ans);
}
}
树形dp
/*
树等价于有向图
*/
import java.util.Scanner;
import java.util.Arrays;
public class Main{
static int N = 6010;
static int happy[] = new int[N];
static int h[] = new int[N]; // 邻接表(存储的是以u为根的相邻一层的所有子节点)
static int e[] = new int[N]; // 节点数组,存储节点编号
static int ne[] = new int[N]; // nxt数组,存储序号
static boolean hasFather [] = new boolean[N];
static int f[][] = new int[N][2];
static int idx = 0;
public static void add(int a, int b){
e[idx] = b; // 新建节点
ne[idx] = h[a];
h[a] = idx;
idx ++;
}
public static void dfs(int u){
f[u][1] = happy[u];
for(int i = h[u]; i != - 1; i = ne[i]){
int j = e[i]; // 节点编号
dfs(j);
f[u][1] += f[j][0];
f[u][0] += Math.max(f[j][1], f[j][0]);
}
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
for(int i = 1; i <= n; i ++){
happy[i] = reader.nextInt();
}
Arrays.fill(h, - 1); // 注意初始化的位置,链表头结点都初始化为-1
for(int i = 0; i < n - 1; i ++){
int a = reader.nextInt();
int b = reader.nextInt(); //父
add(b, a);
hasFather[a] = true;
}
int root = 1;
while(hasFather[root]){ root ++;}
System.out.println(root);
dfs(root);
System.out.println(Math.max(f[root][0], f[root][1]));
}
}
DFS暴力过2/5
/*
至少两周,则每周最多减重重量为[1, n/2] // 0不是正整数
每一周要比上一周多
*/
import java.util.Scanner;
public class Main{
static int ans = 0;
static int n;
public static void dfs(int k, int cur, int sum){
if(sum > n){
return;
}
if(sum == n){
if(k >= 2){
ans ++;
}
return;
}
for(int i = cur + 1; (i - 1) * 2 <= n; i ++){
dfs(k + 1, i, sum + i);
}
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
n = reader.nextInt();
for(int i = 1; i * 2 < n; i ++){ //第一周减肥重量
dfs(1, i, i);
}
System.out.println(ans);
}
}
/*
dp(i,j)表示i分解为j个正整数的方案集合
当前正整数为 j,即最后一个正整数是 j。那么我们需要在剩余的 i-j 中分解为 j-1 个正整数。
即dp[i-j][j-1]
当前正整数不是 j,即最后一个正整数小于 j。那么我们需要在剩余的 i 中分解为 j 个正整数。
即dp[i-1][j]
*/
import java.util.Scanner;
public class Main {
public static int countWeightLossPlans(int n) {
int[][] dp = new int[n + 1][n + 1];
// dp[i][1] 表示将 i 分解为一个正整数的方案数,由于只有一种情况(即 i 本身),所以初始化为 1。
for (int i = 1; i <= n; i++) {
dp[i][1] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 2; j <= i; j++) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - j][j];
}
}
int count = 0;
// j 的范围是 2 到 n,因为至少需要两个正整数才能减重。
for (int j = 2; j < n; j++) {
count += dp[n][j];
}
return count;
}
public static void main(String[] args) {
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
int count = countWeightLossPlans(n);
System.out.println(count);
}
}