核心思想

状压dp 设计一个整型可变参数status,利用status的位信息,来表示: 某个样本是否还能使用,然后利用这个信息进行尝试 写出尝试的递归函数 -> 记忆化搜索 -> 严格位置依赖的动态规划 -> 空间压缩等优化

如果有k个样本,那么表示这些样本的状态,数量是2^k 所以可变参数status的范围: 0 ~ (2^k)-1

样本每增加一个,状态的数量是指数级增长的,所以状压dp能解决的问题往往样本数据量都不大 一般样本数量在20个以内 (10^6),如果超过这个数量,计算量 (指令条数) 会超过 10^7 ~ 10^8 讲解043 - 根据数据量猜解法的技巧,天字第一号重要技巧

如果样本数量大到状压dp解决不了,或者任何动态规划都不可行,那么双向广搜是一个备选思路

例题

1、我能赢吗

题目描述: 在 "100 game" 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和 达到或超过  100 的玩家,即为胜者。

如果我们将游戏规则改为 “玩家 不能 重复使用整数” 呢?

例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。

给定两个整数 maxChoosableInteger (整数池中可选择的最大数)和 desiredTotal(累计和),若先出手的玩家能稳赢则返回 true ,否则返回 false 。假设两位玩家游戏时都表现 最佳 。

示例 1:

输入:maxChoosableInteger = 10, desiredTotal = 11 输出:false 解释: 无论第一个玩家选择哪个整数,他都会失败。 第一个玩家可以选择从 1 到 10 的整数。 如果第一个玩家选择 1,那么第二个玩家只能选择从 2 到 10 的整数。 第二个玩家可以通过选择整数 10(那么累积和为 11 >= desiredTotal),从而取得胜利. 同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。

示例 2:

输入:maxChoosableInteger = 10, desiredTotal = 0 输出:true

示例 3:

输入:maxChoosableInteger = 10, desiredTotal = 1 输出:true

提示:

  • 1 <= maxChoosableInteger <= 20

  • 0 <= desiredTotal <= 300

注意:这道题有两个可变参数status、rest 但最关键的可变参数就1个,即status,表示还有哪些数字可以使用 另一个可变参数rest是被status决定的,所以只需要对status做缓存表 任何动态规划都是这样!只关注最关键的可变参数,被决定的可变参数不用管!不重要!

解题思路:

  • 只能在数据量十分小的情况下

  • 就是依靠位运算来表示每种可能的情况路径,每一位表示这个路径走过没。在本题表示为某个数字是否使用过,用0表示使用过了,后续递归调用不能再使用

  • 暴力枚举每种路径情况

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 464 + 我能赢吗

🧠 题目关键词(原文关键词)

不能 重复使用整数,maxChoosableInteger (整数池中可选择的最大数)和 desiredTotal(累计和),先出手的玩家能稳赢则返回 true,- 1 <= maxChoosableInteger <= 20

🧩 抽象问题类型(题目本质)

路径状态

🔍 数据规模 / 限制

- 1 <= maxChoosableInteger <= 20 - 0 <= desiredTotal <= 300

🧭 我的初步思路

状压dp

✅ 正确解法类型

状压dp

❗ 没想到的原因

递归中的分类讨论关于reset<=0出错误

📦 归入的题型分类

状压dp

🧠 触发词(以后遇到就联想)

数据量<20, 考虑路径状态变化->状压dp

🧪 解法一句话总结

依靠位运算表示每种可能情况路径的变化

代码如下

 package main.java.class080;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code01_CanIWin  
  * @description: 我能赢吗  
  * // 给定两个整数n和m  
  * // 两个玩家可以轮流从公共整数池中抽取从1到n的整数(不放回)  
  * // 抽取的整数会累加起来(两个玩家都算)  
  * // 谁在自己的回合让累加和 >= m,谁获胜  
  * // 若先出手的玩家能稳赢则返回true,否则返回false  
  * // 假设两位玩家游戏时都绝顶聪明,可以全盘为自己打算  
  * // 测试链接 : https://leetcode.cn/problems/can-i-win/  
  * @author: zs宝  
  * @create: 2025-09-26 10:38  
  * @Version 1.0  
  **/public class Code01_CanIWin {  
     class Solution {  
         public boolean canIWin(int n, int m) {  
             if(m==0){  
                 return true;  
             }  
             // 如果1~n数字全加起来  
             // 累加和和是n * (n+1) / 2,都小于m  
             // 那么不会有赢家,也就意味着先手不会获胜  
             if(n * (n + 1) / 2 < m){  
                 return false;  
             }  
             //0:表示未使用过  
             //1:表示使用过且win  
             //-1:表示使用过且lose  
             int[]dp=new int[1 << (n+1)];  
             return f(n,(1<<(n+1))-1,m,dp);  
         }  
   
         // 如果1~7范围的数字都能选,那么status的状态为:  
         // 1 1 1 1 1 1 1 1  
         // 7 6 5 4 3 2 1 0        // 0位弃而不用  
         // 如果1~7范围的数字,4、2已经选了不能再选,那么status的状态为:  
         // 1 1 1 0 1 0 1 1  
         // 7 6 5 4 3 2 1 0        // 0位弃而不用  
         // f的含义 :        // 数字范围1~n,当前的先手,面对status给定的数字状态  
         // 在累加和还剩rest的情况下  
         // 返回当前的先手能不能赢,能赢返回true,不能赢返回false  
         //n:表示可以使用的数字的范围  
         //status:状态:表示还有那些数字可用,位运算。1表示可以用,0表示不可以用  
         //reset:还剩下的累积和  
         public boolean f(int n,int status,int reset,int[]dp){  
             if(reset<=0){  
                 return false;  
             }  
             if(dp[status]!=0){  
                 return dp[status]==1;  
             }  
             boolean ans=false;  
             for(int i=1;i<=n;i++){  
                 // 考察所有数字,但是不能选择之前选了的数字  
                 if((status & (1<<i))!=0 && !f(n,(status^(1<<i)),reset-i,dp)){  
                     ans=true;  
                     break;  
                 }  
             }  
             dp[status]=ans?1:-1;  
             return ans;  
         }  
     }  
 }

2、 火柴拼正方形

题目描述: 你将得到一个整数数组 matchsticks ,其中 matchsticks[i] 是第 i 个火柴棒的长度。你要用 所有的火柴棍 拼成一个正方形。你 不能折断 任何一根火柴棒,但你可以把它们连在一起,而且每根火柴棒必须 使用一次 。

如果你能使这个正方形,则返回 true ,否则返回 false 。

示例 1:

输入: matchsticks = [1,1,2,2,2] 输出: true 解释: 能拼成一个边长为2的正方形,每边两根火柴。

示例 2:

输入: matchsticks = [3,3,3,3,4] 输出: false 解释: 不能用所有火柴拼成一个正方形。

提示:

  • 1 <= matchsticks.length <= 15

  • 1 <= matchsticks[i] <= 108

解题思路如下

  • 这个题的数据量非常小,且我们需要每次枚举选择的那一根火柴

  • 使用状压dp

  • 这个题写代码时,犯了2个错误

    • 首先是初始调用(1<<n)-1写的(1<<(n+1))-1, 状态数量估计错误

    • 第二个是(status & (1<<i))!=0写成(status & (1<<i))== 1,但是位运算判断前面位置的状态,它的结果可能不是1,可能是2,4,8,16等等

  • 利用位运算表示每一个数的使用情况,每次循环遍历每一根未使用火柴,但是要求当前边的长度+火柴长度<=边的长度,若等于,则边长度清0,下一次递归进入下一个边的判断,若不等,则长度+火柴长度,仍然下次递归考虑当前边

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 473 + 火柴拼正方形

🧠 题目关键词(原文关键词)

整数数组,火柴棍 拼成一个正方形, 不能折断,每根火柴棒必须 使用一次

🧩 抽象问题类型(题目本质)

数组分为K份,且每份总和相同

🔍 数据规模 / 限制

- 1 <= matchsticks.length <= 15 - 1 <= matchsticks[i] <= 108

🧭 我的初步思路

状压dp

✅ 正确解法类型

状压dp

❗ 没想到的原因

状态数量,位运算判断错误

📦 归入的题型分类

状压dp

🧠 触发词(以后遇到就联想)

数组分为K份,且每份总和相同,数据量小->状压dp

🧪 解法一句话总结

利用位运算表示每一个数的使用情况,每次循环遍历每一根火柴,但是要求当前边的长度+火柴长度<=边的长度,若等于,则边长度清0,下一次递归进入下一个边的判断,若不等,则长度+火柴长度,仍然下次递归考虑当前边

代码如下

 package main.java.class080;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code02_MatchsticksToSquare  
  * @description: 火柴拼正方形  
  * // 你将得到一个整数数组 matchsticks  
  * // 其中 matchsticks[i] 是第 i 个火柴棒的长度  
  * // 你要用 所有的火柴棍 拼成一个正方形  
  * // 你 不能折断 任何一根火柴棒,但你可以把它们连在一起,而且每根火柴棒必须 使用一次  
  * // 如果你能拼出正方形,则返回 true ,否则返回 false  
  * // 测试链接 : https://leetcode.cn/problems/matchsticks-to-square/  
  * @author: zs宝  
  * @create: 2025-09-27 09:55  
  * @Version 1.0  
  **/public class Code02_MatchsticksToSquare {  
     class Solution {  
         public boolean makesquare(int[] matchsticks) {  
             int n=matchsticks.length;  
             int sum=0;  
             for(int num:matchsticks){  
                 sum+=num;  
             }  
             if(sum%4!=0){  
                 return false;  
             }  
             int limit=sum/4;  
             int[]dp=new int[1<<n];  
             return f(matchsticks,n,limit,(1<<n)-1,0,4,dp);  
         }  
   
   
         /**  
          limit:限定的当前边的长度  
          status:位运算表示那些火柴使用过了  
          cur:当前边的长度  
          reset:还差多少条边  
          */  
         public boolean f(int[] matchsticks,int n,int limit,int status,int cur,int reset,int[]dp){  
             if(reset==0){  
                 return status==0;  
             }  
             if(dp[status]!=0){  
                 return dp[status]==1;  
             }  
             boolean ans=false;  
             //状压dp,开始选边  
             for(int i=0;i<n;i++){  
                 if((status & (1<<i))!=0 && cur+matchsticks[i]<=limit){  
                     if(cur+matchsticks[i]==limit){  
                         ans=f(matchsticks,n,limit,status^(1<<i),0,reset-1,dp);  
                     }else{  
                         ans=f(matchsticks,n,limit,status^(1<<i),cur+matchsticks[i],reset,dp);  
                     }  
                     if(ans){  
                         break;  
                     }  
                 }  
             }  
             dp[status]=ans?1:-1;  
             return ans;  
         }  
     }  
 }

3、划分为k个相等的子集

题目如下: 给定一个整数数组  nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。

示例 1:

输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4 输出: True 说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。

示例 2:

输入: nums = [1,2,3,4], k = 3 输出: false

提示:

  • 1 <= k <= len(nums) <= 16

  • 0 < nums[i] < 10000

  • 每个元素的频率在 [1,4] 范围内

解题思路:

  • 这个题与第二个题几乎一致的状压dp思路

  • 但是这个题可以用暴力递归+剪枝通过

  • 状压dp vs 纯暴力的递归结合剪枝(不做任何动态规划)

    • 状压dp:根据数据量进行复杂度的计算,发现可以通过,那就稳稳通过。推荐。因为能稳定通过。

    • 纯暴力的递归(不做任何动态规划):根据数据量进行复杂度的计算,发现不能通过,但是有大量剪枝的策略,有可能在数据状况并不严苛的情况下能通过,甚至时间还比状压dp快,这是有可能的。但是如果出题人刻意设置数据状况,那么一定无法通过。不推荐。因为不能稳定通过,并且方法本身没什么亮点。

项目名称

内容填写(每道题一行填写)

📘 题目编号 / 标题

LeetCode 698 + 划分为K个相等的子集

🧠 题目关键词(原文关键词)

整数数组  nums 和一个正整数 k,数组分成 k 个非空子集,其总和都相等。

🧩 抽象问题类型(题目本质)

数组分为K份,且每份总和相同

🔍 数据规模 / 限制

- 1 <= k <= len(nums) <= 16 - 0 < nums[i] < 10000 - 每个元素的频率在 [1,4] 范围内

🧭 我的初步思路

状压dp

✅ 正确解法类型

状压dp

❗ 没想到的原因

识别出来了

📦 归入的题型分类

状压dp

🧠 触发词(以后遇到就联想)

数组分为K份,且每份总和相同,数据量小->状压dp

🧪 解法一句话总结

利用位运算表示每一个数的使用情况,每次循环遍历每一个数,但是要求当前数组和+数值<=每个数组的总和,若等于,则数组和清0,辅助下一次递归进入下一个数组的判断,若不等,则当前数组和+数值,仍然下次递归考虑当划分数组

代码如下

 package main.java.class080;  
   
 import java.util.Arrays;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code03_PartitionToKEqualSumSubsets  
  * @description: 划分为k个相等的子集  
  * // 给定一个整数数组  nums 和一个正整数 k,  
  * // 找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。  
  * // 测试链接 : https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/  
  * @author: zs宝  
  * @create: 2025-09-27 10:41  
  * @Version 1.0  
  **/public class Code03_PartitionToKEqualSumSubsets {  
     class Solution {  
         //状压dp解法  
         public boolean canPartitionKSubsets1(int[] nums, int k) {  
             int n = nums.length;  
             int sum = 0;  
             for (int num : nums) {  
                 sum += num;  
             }  
             if (sum % k != 0) {  
                 return false;  
             }  
             int limit = sum / k;  
             int[] dp = new int[1 << n];  
             return f1(nums, n, limit, (1 << n) - 1, 0, k, dp);  
         }  
   
         /**  
          * limit:限定的当前边的长度  
          * status:位运算表示那些火柴使用过了  
          * cur:当前边的长度  
          * reset:还差多少条边  
          */  
         public boolean f1(int[] nums, int n, int limit, int status, int cur, int reset, int[] dp) {  
             if (reset == 0) {  
                 return status == 0;  
             }  
             if (dp[status] != 0) {  
                 return dp[status] == 1;  
             }  
             boolean ans = false;  
             //状压dp,开始选边  
             for (int i = 0; i < n; i++) {  
                 if ((status & (1 << i)) != 0 && cur + nums[i] <= limit) {  
                     if (cur + nums[i] == limit) {  
                         ans = f1(nums, n, limit, status ^ (1 << i), 0, reset - 1, dp);  
                     } else {  
                         ans = f1(nums, n, limit, status ^ (1 << i), cur + nums[i], reset, dp);  
                     }  
                     if (ans) {  
                         break;  
                     }  
                 }  
             }  
             dp[status] = ans ? 1 : -1;  
             return ans;  
         }  
   
         //暴力递归的解法+剪枝优化  
         public boolean canPartitionKSubsets2(int[] nums, int k) {  
             int n = nums.length;  
             int sum = 0;  
             for (int num : nums) {  
                 sum += num;  
             }  
             if (sum % k != 0) {  
                 return false;  
             }  
             int target = sum / k;  
             Arrays.sort(nums);  
             return f2(nums, new int[k], n - 1, target);  
         }  
   
         //将nums数组分为K份,要求每份累加和相等  
         //group数组存储每份的累加和  
         public boolean f2(int[] nums, int[] group, int index, int target) {  
             if (index < 0) {  
                 return true;  
             }  
             int len = group.length;  
             int num = nums[index];  
             for (int i = 0; i < len; i++) {  
                 if (group[i] + num <= target) {  
                     group[i] += num;  
                     if (f2(nums, group, index - 1, target)) {  
                         return true;  
                     }  
                     //复原  
                     group[i] -= num;  
                     while (i + 1 < len && group[i] == group[i + 1]) {  
                         i++;  
                     }  
                 }  
             }  
             return false;  
         }  
     }  
   
   
 }

4、售货员的难题 - TSP问题

题目描述

某乡有 n 个村庄,有一个售货员,他要到各个村庄去售货,各村庄之间的路程 s_{i,j} 是已知的,且 A 村到 B 村与 B 村到 A 村的路大多不同。为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为 1,他不知道选择什么样的路线才能使所走的路程最短。请你帮他选择一条最短的路。

输入格式

第一行是一个整数,表示村庄数 n。 接下来 n 行,每行 n 个整数,第 i 行的第 j 个整数表示 i 到 j 的单向路径的距离 s_{i,j}。 输出格式

一行一个整数表示最短的路程。 输入输出样例 #1 输入 #1

 3
 0 2 1
 1 0 2
 2 1 0

输出 #1

 3

说明/提示

对全部的测试数据,保证 2 \leq n \leq 20,1 \leq s_{i,j} < 10^3。

解题思路:

  • 这道题是图论中大名鼎鼎的旅行商问题

  • 由于数据量较小,因此可以用状压dp来做

  • 但是本题我写的是时候,老是将第一次调用的递归的当前状态写错

  • 这个题在洛谷上,按照标准的状压dp写法,最后一个测试数据会空间超过爆红,但是同样的写法思路C++可过,Java不能过

  • 因此有第二种写法,另辟蹊径,将源点的相关路径提取到另外两个数组start,back中去。但是其实两种写法的思路是一样的

代码如下: 标准状压dp写法

 package main.java.class080;  
   
 import java.io.*;  
 import java.util.Arrays;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code04_TSP1  
  * @description: 售货员的难题 - TSP问题  
  * // 某乡有n个村庄(1<=n<=20),有一个售货员,他要到各个村庄去售货  
  * // 各村庄之间的路程s(1<=s<=1000)是已知的  
  * // 且A村到B村的路程,与B到A的路大多不同(有向带权图)  
  * // 为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,  
  * // 假设商店所在的村庄为1  
  * // 他不知道选择什么样的路线才能使所走的路程最短  
  * // 请你帮他选择一条最短的路  
  * // 测试链接 : https://www.luogu.com.cn/problem/P1171  
  * @author: zs宝  
  * @create: 2025-09-27 11:12  
  * @Version 1.0  
  **/// 正常来说把MAXN改成20能通过,实现是正确的  
 // 问题是卡空间,而且c++的实现不卡空间,就卡java的实现  
 // 但如果把MAXN改成19,会有一个测试用例通过不了  
 // 那就差这么一点空间...看不起java是吗?  
 // 好,你歧视java实现,那就别怪我了  
 // 完全能通过的版本看Code04_TSP2的实现  
 public class Code04_TSP1 {  
     public static int MAXN=20;  
     public static int[][]graph=new int[MAXN][MAXN];  
     public static int[][]dp=new int[1<<MAXN][MAXN];  
     public static int n;  
   
   
     public static void build(){  
         for(int i=0;i<1<<n;i++){  
             for(int j=0;j<n;j++){  
                 dp[i][j]=-1;  
             }  
         }  
     }  
     public static void main(String[] args) throws IOException {  
         BufferedReader buffer=new BufferedReader(new InputStreamReader(System.in));  
         StreamTokenizer in=new StreamTokenizer(buffer);  
         PrintWriter out=new PrintWriter(new OutputStreamWriter(System.out));  
         while (in.nextToken()!=StreamTokenizer.TT_EOF){  
             n=(int)in.nval;  
             build();  
             for(int i=0;i<n;i++){  
                 for(int j=0;j<n;j++){  
                     in.nextToken();  
                     graph[i][j]=(int)in.nval;  
                 }  
             }  
   
             out.println(compute());  
         }  
         out.flush();  
         out.close();  
         buffer.close();  
     }  
     public static int compute(){  
         //初始状态由于售货员在第一个村庄,因此初始状态第一个村庄置为1  
         //1:表示走过村庄  
         //0:表示未走过村庄  
         return f(0,1);  
     }  
   
     /**  
      *     * @param cur 当前村庄  
      * @param status 当前位运算表示状态,那些村庄已经走过了  
      * @return  
      */  
     public static int f(int cur,int status){  
         if(status==(1<<n)-1){  
             return graph[cur][0];  
         }  
         if(dp[status][cur]!=-1){  
             return dp[status][cur];  
         }  
         int ans=Integer.MAX_VALUE;  
         //开始遍历寻找最小值  
         for(int i=0;i<n;i++){  
             //只能走未走过的村庄  
             if((status & (1<<i))==0){  
                 ans=Math.min(ans,graph[cur][i]+f(i,status| (1<<i)));  
             }  
         }  
         dp[status][cur]=ans;  
         return ans;  
     }  
   
 }

这个题专门为Java而过的改动写法

 package main.java.class080;  
   
 import java.io.*;  
   
 /**  
  * @program: ZuoChengxunAlgorithmClass  
  * @ClassName Code04_TSP2  
  * @description: 售货员的难题 - TSP问题  
  * // 某乡有n个村庄(1<=n<=20),有一个售货员,他要到各个村庄去售货  
  * // 各村庄之间的路程s(1<=s<=1000)是已知的  
  * // 且A村到B村的路程,与B到A的路大多不同(有向带权图)  
  * // 为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,  
  * // 假设商店所在的村庄为1  
  * // 他不知道选择什么样的路线才能使所走的路程最短  
  * // 请你帮他选择一条最短的路  
  * // 测试链接 : https://www.luogu.com.cn/problem/P1171  
  * @author: zs宝  
  * @create: 2025-09-27 11:53  
  * @Version 1.0  
  **/public class Code04_TSP2 {  
     public static int MAXN=19;  
     public static int[][]graph=new int[MAXN][MAXN];  
     public static int[][]dp=new int[1<<MAXN][MAXN];  
     public static int n;  
     //记录第0行位置的元素,即售货点与各个村庄的位置  
     public static int[] start = new int[MAXN];  
     //记录各个村庄到售货点的位置  
     public static int[] back = new int[MAXN];  
   
     public static void build(){  
         for(int i=0;i<1<<n;i++){  
             for(int j=0;j<n;j++){  
                 dp[i][j]=-1;  
             }  
         }  
     }  
     public static void main(String[] args) throws IOException {  
         BufferedReader buffer=new BufferedReader(new InputStreamReader(System.in));  
         StreamTokenizer in=new StreamTokenizer(buffer);  
         PrintWriter out=new PrintWriter(new OutputStreamWriter(System.out));  
         while (in.nextToken()!=StreamTokenizer.TT_EOF){  
             //现在我们将售货点村庄单独排在外面,有关售货点与村庄的距离关系全部放在start和back数组中  
             n=(int)in.nval-1;  
             build();  
             in.nextToken();  
             //记录售货点村庄到其它村庄的距离  
             for(int i=0;i<n;i++){  
                 in.nextToken();  
                 start[i]=(int)in.nval;  
             }  
   
             for(int i=0;i<n;i++){  
                 in.nextToken();  
                 back[i]=(int)in.nval;  
                 for(int j=0;j<n;j++){  
                     in.nextToken();  
                     graph[i][j]=(int)in.nval;  
                 }  
             }  
             out.println(compute());  
         }  
         out.flush();  
         out.close();  
         buffer.close();  
     }  
   
     public static int compute(){  
         int ans=Integer.MAX_VALUE;  
         for(int i=0;i<n;i++){  
             ans=Math.min(ans,start[i]+f(1<<i,i));  
         }  
         return ans;  
     }  
     public static int f(int status,int cur){  
         if(status==(1<<n)-1){  
             return back[cur];  
         }  
         if(dp[status][cur]!=-1){  
             return dp[status][cur];  
         }  
         int ans=Integer.MAX_VALUE;  
         for(int i=0;i<n;i++){  
             if((status & (1<<i))==0){  
                 ans=Math.min(ans,f(status | (1<<i),i)+graph[cur][i]);  
             }  
         }  
         dp[status][cur]=ans;  
         return ans;  
     }  
 }

参考资料