DP Day7

一、背包

1.01背包

设f(i,j)为只考虑前i个物品的前提下,每个物品只能拿一次,最多用j容量能获得的最大价值。

显然,f(0,0),f(0,1),f(0,2)...f(0,m)的值为0。 

假设已知f(i-1,0),f(i-1,1)...f(i-1,V),考虑如何求出f(i,j)(0≤j≤V)。 f(i,j)和f(i-1,j)的区别是f(i,j)还可以考虑第i个物品。 也就是说,f(i,j)只会根据取与不取第i个物品从之前的状态转移过来: ①不拿第i个物品最优:f(i-1,j) ②拿第i个物品下最优:f(i-1,j-w[i])+v[i](需要保证j>=w[i]) 综上所述,f(i,j)=max(f(i-1,j-w[i])+v[i],f(i-1,j)) 

那么,以f(0,0),f(0,1),f(0,2)...f(0,V)为基础,我们可以依次    求出f(i,j)(1≤i≤N,0≤j≤V)。

f(N,V)就是我们要求的答案。

 2.完全背包

状态表示: f[i][j]表示只考虑前i个物品的前提下,每个物品能拿任意次,最多用j容量能获得的最大价值。

状态转移方程: f[i][j]=max( f[i-1][j],f[i][j-w[i]]+v[i] )                 (j>=w[i])

 

 优化总结

01背包: 时间复杂度O(N*V),空间复杂度O(V)

完全背包: 时间复杂度O(N*V),空间复杂度O(V)

 3.多重背包

有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。

可以用二进制拆分的方法,依次按照1,2,4,8,..来拆分,直到不足时剩下的来当最后一份。

举个例子:
拆分8(1000b):
第一次拆一份大小为20的,剩余7(111b)
第二次拆一份大小为21的,剩余5(101b)
第三次拆一份大小为22的,剩余1(1b)
第四次由于1<23而结束算法,直接将剩下的1个为1份。

根据算法可知,我们最终拆分出来的大小分别为2^0,2^1,..2^k,x。

二、状压DP

状压DP是利用计算机二进制的性质来描述状态的一种DP方式。

举个例子,假如我们现在有3个资源,那么我们可以通过二进制的表示方法表示出这3个资源的状态。对于二进制数011B,可以表示出第1个、第2个资源被占用且第3个资源未被占用的状态(编号一般从低位开始算)。

hdu5418 Victor and World

题意:有n个城市,在n个城市之间有m条双向路,每条路有一个距离,现在问从1号城市去游览其它的2到n号城市最后回到1号城市的最短路径(保证1可以直接或间接到达2到n)。(n<=16)

分析:考虑O(2^n * n^2)的做法

先通过floyed算法O(N^3)求出mat[][]数组,mat[i][j]表示第i个城市到第j个城市的最短距离。

通过二进制数表示每个城市的访问情况,1为已经访问,0表示还没访问,比如011B就表示第1、2城市已经访问,第3个城市还未访问。(即状压,以后提到的状态都是指一个二进制数,意义同上)

dp[i][s]表示,访问状态为s,而且最后一个访问的城市是i号城市的最短距离。

那么转移方程为:dp[ j ][ s|(1<<(j-1)) ]=min(dp[ j ][ s|(1<<(j-1)) ],dp[ i ][ s ]+mat[i][j])

(笔者注:第一维 j 的意思是当前检视的新城市为j,第二维度 的意义是之前已经储存的状态s,即到检视j城市为止所保存的以前的访问状态,| 的意思是任意有一个即为访问,所以用来统计检视 j 之前的城市的访问状态和新城市访问状态,(1<<(j-1))即为2^(j-1),即新城市 j 在状态压缩操作中所占的第几位,j-1是因为第一位为2^0)

物理意义:访问完i号城市以后继续去访问j号城市

初始化: memset(dp,0x3f,sizeof(dp)); dp[1][1]=0; //因为起始位置在1号城市

求得dp[][]数组的具体代码
memset(dp,0x3f,sizeof(dp)); //初始化无穷大
dp[1][1]=0;//起始状态赋值为0
for(int s=1;s<=(1<<n)-1;++s){ //从起始状态开始往上枚举
	for(int i=1;i<=n;++i){ //枚举当前状态的最后一个城市 
		if( !(s&(1<<(i-1))) ) continue; //对应的位如果不是1,产生矛盾,continue
		for(int j=1;j<=n;++j){ //枚举下一个要去的城市 
			if( s&(1<<(j-1)) ) continue;//对应的位是1,已经访问过,continue
			// ( s|(1<<j-1) )是新的状态 
			dp[j][s|(1<<(j-1))]=min(dp[j][s|(1<<(j-1))],dp[i][s]+mat[i][j]); 
		} 
	}
}

因为最后还需要返回1号城市,所以最后的答案为:
ans=0x3f3f3f3f;
for(int i=1;i<=n;++i) ans=min(ans,dp[i][(1<<n)-1]+mat[i][1]);


(笔者注:枚举以第i个城市为最后检视的终点,这个dp距离+从i到1的mat距离即为答案。(1<<n)-1的意思是所有状态全为1的圆满) 

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 20
int cas; int n; int m;
int mat[N][N]; int dp[N][1<<N];
int main(){
	scanf("%d",&cas);
	while(cas--){
		memset(mat,0x3f,sizeof(mat));
		scanf("%d %d",&n,&m);
		for(int i=1;i<=n;++i) mat[i][i]=0;
		for(int i=1;i<=m;++i){
			int u; int v; int w;
			scanf("%d %d %d",&u,&v,&w);
			mat[u][v]=mat[v][u]=min(mat[u][v],w);
		}
		for(int k=1;k<=n;++k){
			for(int i=1;i<=n;++i){
				for(int j=1;j<=n;++j){
					mat[i][j]=min(mat[i][j],mat[i][k]+mat[k][j]);
				}
			}
		} //floyed 求 mat[][]
		memset(dp,0x3f,sizeof(dp)); dp[1][1]=0;
		for(int s=1;s<=(1<<n)-1;++s){
			for(int i=1;i<=n;++i){ 
				if( !(s&(1<<(i-1))) ) continue; 
				for(int j=1;j<=n;++j){ 
					if( s&(1<<(j-1)) ) continue;
					dp[j][s|(1<<(j-1))]=min(dp[j][s|(1<<(j-1))],dp[i][s]+mat[i][j]); 
				} 
			}
		}
		int ans=0x3f3f3f3f;
		for(int i=1;i<=n;++i) ans=min(ans,dp[i][(1<<n)-1]+mat[i][1]);
		printf("%d\n",ans);
	}
	return 0;
}

洛谷P1896 互不侵犯

题意:在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。 ( 1 <=N <=9, 0 <= K <= N * N)

 分析:考虑O( N^3 * (1<<N)^2 )的做法

行内限制:一行的状态不能有两个相邻的1,否则会侵犯,举俩个状态为例子   (1)110B (×)因为存在两个相邻的1     (2)101B (√)不存在相邻的1

行间限制:设i行的状态为si,令j=i-1(即j和i是上下两行),设j行的状态为sj,易知有以下限制: (1)si&sj=0     (2)(si>>1)&sj=0     (3)(si<<1)&sj=0     (4)(sj>>1)&si=0     (5)(sj<<1)&si=0      否则会相互侵犯。

由于题目还限制了最终国王的个数,所以我们的dp数组还需要腾出一维来记录当前国王的个数。 所以,开设dp[N][N*N*2][1<<N]数组,dp[i][j][s]的意义为:从上往下放置国王(i越来越大)时,放置完第i行的时候(第i+1行还没开始放),第i行的状态为s,且当前已经放置了j个国王的方案数。 状态转移方程: 在满足全部限制下,dp[ i ][  j+num[s2] ][ s2 ] += dp[ i-1 ][ j ][ s1 ] 其中(1)num[x]为x在二进制表示下1的个数。(2)s2是第i行的状态,s1是第i-1行的状态。 初始化: memset(dp,0,sizeof(dp)); dp[0][0][0]=1;

#include<iostream>
#include<cstdio>
using namespace std;
int n,k,num;
long long cnt[2000],ok[2000];   
//cnt[i]:第i种状态的二进制中有几个1
//ok[i]:第i个行内不相矛盾(满足条件2:左右国王不相邻)的状态是多少 
long long dp[10][100][2000];
//dp[i][j][s]:我们已经放了i行,用了j个国王,第i行的状态为s的方案数
int main() 
{
    cin>>n>>k;                    //n*n的棋盘上放k个国王 
    for(int s=0;s<(1<<n);s++)     //枚举所有可能状态 
    {
        int tot=0,s1=s;           //tot:二进制下有多少个1; 
        while(s1)                 //一位一位枚举,直至为0(做法类似于快速幂那样) 
        {
            if(s1&1) tot++;       //如果最后一位是1,tot++ 
            s1>>=1;               //右移看下一位 
        }
        cnt[s]=tot;               //预处理这个二进制数有多少个1
        if((((s<<1)|(s>>1))&s)==0) ok[++num]=s; //如果这一状态向左向右都没有重复的话,说明左右不相邻,合法 
    }
    dp[0][0][0]=1;                //第0行一个也不放的方案数
    for(int i=1;i<=n;i++)         //枚举我们已经放到了第几行 
    {
        for(int l=1;l<=num;l++)   //枚举第i行的状态,这里我们直接枚举所有满足条件2的状态,算是个优化吧 
        {
            int s1=ok[l];             
            for(int r=1;r<=num;r++) //枚举上一行的状态 
            {
                int s2=ok[r];
                if(((s2|(s2<<1)|(s2>>1))&s1)==0)  //如果上下,左下右上,左上右下方向都不相邻,合法 
                {    
                       for(int j=0;j<=k;j++) //枚举国王个数 
                        if(j-cnt[s1]>=0)  
                            dp[i][j][s1]+=dp[i-1][j-cnt[s1]][s2];  //状态转移方程       
                }
            }
        }
    }
    long long ans=0;
    for(int i=1;i<=num;i++) ans+=dp[n][k][ok[i]];  //枚举第n行所有可能的情况,统计答案 
    printf("%lld\n",ans);    //输出 
    return 0;
}

三、单调队列优化DP

当dp方程为dp[ i ]=a[ i ]+b[ j ]时,这个方程是O(n^2)的。  这时可以用单调队列可以将其优化为O(n)。

洛谷P2627 修剪草坪

题意:Farm John有N(1 <= N <= 100,000)只排成一排的奶牛,编号为1...N。每只奶牛的效率是不同的,奶牛i的效率为E_i(0 <= E_i <= 1,000,000,000)。 靠近的奶牛们很熟悉,因此,如果Farm John安排超过K只连续的奶牛,那么,这些奶牛就会罢工去开派对:)。因此,现在Farm John需要你的帮助,计算FJ可以得到的最大效率,并且该方案中没有连续的超过K只奶牛。 A<=B<=10^12。

 补充一个知识点: 对于一个数组a[],如果我们要O(1)查询a[l] ,a[l+1],...,a[r-1],a[r]的和,那么我们可以预处理一个前缀和数组sum[]。 对于sum数组,sum[i]=a[1]+a[2]+a[3]+...+a[i-1]+a[i]。 那么对于上述问题,就可以通过sum[r]-sum[l-1]在O(1)的时间内得到。

dp[1][i] 表示在前 i 头奶牛中,选了第 i 头奶牛能获得的最大效率。 dp[0][i] 表示在前 i 头奶牛中,不选第 i 头奶牛能获得的最大效率。 转移方程:

dp[0][i]=max(dp[0][i-1],dp[1][i-1])

dp[1][i]=max(dp[0][j]+  (sum[i]-sum[j])  )    ( j<i&&i-j<=k )<=>( max(i-k,0)<=j<i  ) 最后的答案是max(dp[1][i])

(注:dp[1][i]=max(dp[0][j]+  (sum[i]-sum[j])  )为枚举j,从0到j不工作,然后从j+1到i工作;枚举过程中选择最大的)

最后的答案是max(dp[1][i]) (注:答案不是dp[1][n])

按照朴素的方法来写一发,以下是代码。
#include <cstdio>
#include <algorithm>
using namespace std;
#define N 100005
#define ll long long
int n; int k; ll a[N]; ll dp[2][N]; ll sum[N];
int main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;++i) scanf("%lld",a+i);
	for(int i=1;i<=n;++i) sum[i]=sum[i-1]+a[i];
	for(int i=1;i<=n;++i){
		dp[0][i]=max(dp[0][i-1],dp[1][i-1]);
		for(int j=max(0,i-k);j<i;++j) dp[1][i]=max(dp[1][i],dp[0][j]+sum[i]-sum[j]);
	}
	ll ans=0;
	for(int i=1;i<=n;++i) ans=max(ans,dp[1][i]);
	printf("%lld\n",ans);
	return 0;
}

不出所料,会tle,要用到队列知识优化

我们看转移方程:

dp[0][i]=max(dp[0][i-1],dp[1][i-1]) (1)
dp[1][i]=max(dp[0][ j ]+  (sum[i]-sum[ j ])  )   max(i-k,0)<=j<i (2)

(1)式的时间复杂度是O(1)的。 (2)式的时间复杂度是O(N)的。 所以我们主要对(2)式进行优化。 对(2)式变化一下: dp[1][i]=sum[i]+max(dp[0][ j ]-sum[ j ]) max(i-k,0)<=j<i (2) 发现在上述表达式中sum[j]是预处理出来的,是已知的。 dp[0][j]由于j<i,所以在求dp[1][i]的时候dp[0][ j ]也已经求出来了,也是已知的。 那么我们能不能考虑,在求出dp[0][ j ]的时候来维护一个最大的dp[0][ j ]-sum[ j ]供后面的dp[1][i]使用? 当然是可以的,我们可以用一个单调递减队列来维护。

可能有人会有疑问,既然要维护一个最大的dp[0][j]-sum[j]供后面的dp[1][i]使用,直接开max变量来维护最大值不就好了吗,为什么特地去用一个单调递减队列维护呢?

我们再看一下转移方程: dp[1][i]=sum[i]+max(dp[0][ j ]-sum[ j ]) max(i-k,0)<=j<i (2)

对于限制max(i-k,0)<=j<i 如果变成 j<i 的话完全是可以只开一个max变量来维护的。

但是这里还有max(i-k,0)<=j这个限制 ,也就意味着随着i变大 使得max(i-k)>j 从而使原本最优的决策 j 无法被使用,也就是说最优的决策 j 随着时间的推移(这里表现为i的增加)不会一直在候选队列中,会因为某些限制被“踢出去”,而原本次优的决策变成了最优的决策。

综上所述,我们只维护一个最优的决策是不行的,第二优、第三优...的决策都可以等到前面比它们优的决策从队首出队以后,自己来成为队列中最优的决策。

同时,如果一个决策后面有一个比它更优的决策,那么这个决策就永远不会成为队列中最优的决策,因为后面会一直存在一个比它更优的决策,所以这个决策继续呆在队列中等待是没有意义的,因为它永远不会成为最优的,所以我们可以把它去掉。

最后,这个队列就变成了一个单调递减队列(在这道题中一个决策j 越优秀表现在dp[0][ j ]+sum[ j ]越大)。

单调递减队列(队列中存的是下标)维护过程

(1)保证队首元素可以使用:while(tail>head&&i-k>q[head]) ++head; 主要就是控制队首元素对应的下标 j 满足max(i-k,0)<=j<i 这个条件

(2)dp[ 1 ][ i ]=sum[i] + dp[ 0 ][ q[head] ] - sum[ q[head] ](因为队首是最优的决策,这样可以保证dp[ 1 ][ i ]是最大的。)

(3) 下标i代表的决策进队的同时维护队列单调递减的性质          

while(tail>head&&dp[0][i]-sum[i]>=dp[0][q[tail-1]]-sum[q[tail-1]]) --tail;           q[tail++]=i;

注意在我的写法中q[head]是队列首元素,q[tail-1]才是队列尾元素(而不是q[tail])

#include <cstdio>
#include <algorithm>
using namespace std;
#define N 100005
#define ll long long
int n; int k; ll a[N]; ll dp[2][N]; ll sum[N]; ll q[N]; int head; int tail;
int main(){
	scanf("%d %d",&n,&k);
	for(int i=1;i<=n;++i) scanf("%lld",a+i);
	for(int i=1;i<=n;++i) sum[i]=sum[i-1]+a[i];
	q[tail++]=0;//先把0放入优先队列 
	for(int i=1;i<=n;++i){
		while(tail>head&&i-k>q[head]) ++head; 
		dp[0][i]=max(dp[0][i-1],dp[1][i-1]);
		dp[1][i]=dp[0][q[head]]+sum[i]-sum[q[head]];
		while(tail>head&&dp[0][i]-sum[i]>=dp[0][q[tail-1]]-sum[q[tail-1]]) --tail;
		q[tail++]=i;
	}
	ll ans=0;
	for(int i=1;i<=n;++i) ans=max(ans,dp[1][i]);
	printf("%lld\n",ans);
	return 0;
}

四、斜率优化DP

dp方程为dp[ i ]=a[ i ]*b[ j ]+c[ i ]+d[ j ]时,这个方程是O(n^2)的。 这时可以用斜率优化可以将其优化为O(n)。

洛谷P3195 [HNOI2008]玩具装箱TOY

 

 

 

 

 

#include <cstdio>
#include <algorithm>
using namespace std;
#define N 50005
#define ll long long
#define a(x) (sum[x]+x)
#define b(x) (sum[x]+x+1+l)
#define c(x) (a(x)*a(x))
#define d(x) (dp[x]+b(x)*b(x))
ll n; ll l; ll c[N]; ll sum[N]; int q[N]; int head; int tail;
ll dp[N];
double slope(int j1,int j2){
	return (d(j2)-d(j1))/( b(j2)-b(j1)==0?1e-9:b(j2)-b(j1) );
}
int main(){
	scanf("%lld %lld",&n,&l);
	for(int i=1;i<=n;++i) scanf("%lld",c+i);
	for(int i=1;i<=n;++i) sum[i]=sum[i-1]+c[i];
	q[tail++]=0;
	for(int i=1;i<=n;++i){
		while(tail-head>=2&&2*a(i)>=slope(q[head],q[head+1])  ) ++head;
		dp[i]=-2*a(i)*b(q[head])+c(i)+d(q[head]);
		while(tail-head>=2&&slope(q[tail-1],i)<=slope(q[tail-2],q[tail-1])  ) --tail;
		q[tail++]=i;
	}
	printf("%lld\n",dp[n]);
	return 0;
}

 总结

(1)单调队列优化适应dp[ i ]=a[ i ]+b[ j ];       斜率优化适应dp[ i ]=a[ i ]*b[ j ]+c[ i ]+d[ j ]。

(2)单调队列优化主要维护单点递减/递增;      斜率优化主要维护两点间斜率单调递减/递增。

<think>好的,用户问的是DP本地7b能不能进行代码审计。首先,我需要确认DP本地7b是什么。可能是指DeepSeek的某个7B参数模型,比如DeepSeek-R1或者类似的本地部署版本。用户提到“本地”,说明他们可能关注的是无需联网、在本地运行的能力。 接下来,代码审计的要求通常包括理解代码逻辑、识别安全漏洞、检查合规性等。7B参数的模型属于中等规模,虽然在理解代码结构、语法方面可能有不错的表现,但处理复杂审计任务可能会有局限。比如,它可能能检测简单的SQL注入或XSS漏洞,但对更复杂的逻辑漏洞或新型漏洞可能效果不佳。 需要考虑用户的使用场景:可能是个体开发者或小团队,想在开发过程中快速检查代码,不需要专业工具。或者他们可能希望有一个免费、本地的解决方案,避免代码泄露的风险。 用户的真实需求可能不仅仅是能否进行,而是效果如何,需要哪些资源,比如硬件要求(RAM、GPU),以及如何优化使用。可能还需要比较与其他工具(如SonarQube、Fortify)的区别,突出本地模型的优缺点。 需要指出模型的局限性,比如依赖训练数据、无法实时更新知识库,以及可能的误报/漏报。同时,建议结合其他工具和方法,形成多层防护,确保代码安全。 最后,要给出实际应用步骤的例子,比如安装、运行模型、输入代码示例,说明输出结果,让用户有直观的认识。还要提醒用户,虽然有用,但不能完全依赖,重要项目需要专业审计。</think>DP 本地7B(可能指 DeepSeek-R1 等本地部署的 70 亿参数大模型)**具备基础的代码审计能力**,但其效果受限于模型规模和训练数据。以下是关键分析: --- ### **核心能力** 1. **基础漏洞检测** 可识别常见漏洞(如 SQL 注入、XSS 等),对简单代码逻辑错误(硬编码密钥、路径遍历)有一定敏感度。 2. **代码理解** 支持主流语言(Python/Java/JS 等),能解析基础语法结构,辅助人工快速定位可疑代码段。 3. **模式识别** 通过训练数据中的漏洞模式匹配,对高危函数(如 `eval()`、`system()`)或危险配置提出警告。 --- ### **主要局限** 1. **复杂逻辑盲区** 对多线程竞争、业务逻辑漏洞、新型 0day 漏洞检测能力较弱。 2. **上下文依赖** 长代码文件或跨文件审计时,可能因上下文理解不足导致误判/漏判。 3. **知识时效性** 无法主动学习最新漏洞库(如新披露的 CVE),需定期更新模型。 --- ### **典型使用场景** - **开发阶段自查**:快速扫描代码中的 "低级错误" - **教育辅助**:帮助新手理解安全编码规范 - **二次过滤**:作为专业工具(SonarQube/Semgrep)的补充 --- ### **实操建议** 1. **硬件需求** 至少 16GB RAM + 支持 CUDA 的 GPU(如 RTX 3060),纯 CPU 模式推理速度较慢。 2. **优化策略** - 使用 `llama.cpp` 量化模型至 4-bit 降低资源占用 - 限定代码片段长度(建议 <512 tokens) - 明确提示词(示例:`请审计以下PHP代码的SQL注入风险:[代码]`) 3. **示例输出** ```php // 输入代码 $id = $_GET['id']; $sql = "SELECT * FROM users WHERE id = $id"; ``` ``` // 模型可能反馈 警告:未对用户输入的$id进行过滤,存在SQL注入风险。 建议:使用预处理语句或mysqli_real_escape_string()处理输入。 ``` --- ### **对比专业工具** | 维度 | DP 本地7B | 专业工具(如 Fortify) | |--| | **检测深度** | 模式匹配为主 | 数据流分析 + 污点追踪 | | **定制能力** | 可通过微调适配内部规范 | 依赖规则库更新 | | **隐私性** | 完全本地化 | 可能需上传代码 | --- **结论:适合作为轻量级辅助工具,但关键系统仍需结合 SAST/DAST 工具和人工审计。** 如需处理复杂项目,建议考虑更大的模型(如 34B 参数级别)。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值