HDU Free from square(状压+分组dp)

hdu 6125 题目链接

题目

一个集合里的数为 1 − n 1-n 1n,选择 x ( 1 ⩽ x ⩽ k ) x(1\leqslant x \leqslant k) x(1xk)个数,使得这 x x x个数的乘积不能被任何除1之外的所有平方数整除。

样例

Input:              Output:
2(T)            
4 2(n=4,k=2)        6   (1,2,3,[1,2],[1,3],[2,3])
6 4                 19

思路

首先说明这道题我是看的qy的代码,没有仔细研究过别人的思路,所以我不知道我说的对不对……

  • 首先本身是平方数倍数的数肯定不能取,如test1的4.然后考虑 x x x个数之积满足不是平方数倍数的条件是什么?

x x x个数之积不含有相同的质因子。

  • 我们如何去判断这 x x x个数是否含有相同的质因子?

参考状压的思想,一个质因子存不存在可以转化为一个2进制数该位是为1还是为0。所以我们把1到 n n n所有数枚举质因数,处理出这个数的质因数所对应的一个 s t a t e state state.

  • d p ( i , j , s ) dp(i,j,s) dp(i,j,s)表示我们枚举到 i i i时取了 j j j个数时的状态是 s s s时的方案数.

状态转移方程: d p ( i , j , s ) = d p ( i − 1 , j , s ) + d p ( i − 1 , j − 1 , l a s t s t a t e ) dp(i,j,s)=dp(i-1,j,s)+dp(i-1,j-1,last state) dp(i,j,s)=dp(i1,j,s)+dp(i1,j1,laststate)

  • 然后我们发现方程逆推的话没有正推方便,即我们用小状态去推大状态

d p ( i + 1 , j , s ) + = d p ( i , j , s ) ( 第 i + 1 个 没 有 选 ) dp(i+1,j,s) += dp(i,j,s) (第i+1个没有选) dp(i+1,j,s)+=dp(i,j,s)i+1
当没有质因子重复时即 s s s& w = = 0 w==0 w==0时, d p ( i + 1 , j + 1 , s ∣ w ) + = d p ( i , j , s ) ( 第 i + 1 个 选 了 , 并 且 第 i + 1 的 状 态 为 w ) dp(i+1,j+1,s|w) += dp(i,j,s) (第i+1个选了,并且第i+1的状态为w) dp(i+1,j+1,sw)+=dp(i,j,s)i+1i+1w

  • 到这里基本思想整理的差不多了,但是实际上离写题还很远。

  • 考虑 d p ( i , j , s ) dp(i,j,s) dp(i,j,s)的最后一维,这个 s s s的大小是取决于500以内的质数数量的。很明显如果全部存下来肯定会MLE和TLE。

任何一个数只有一个大于 n \sqrt{n} n 的质因数,所以一方面我们只枚举 500 \sqrt{500} 500 的质数int prime[8] = {2, 3, 5, 7, 11, 13, 17, 19};,另一方面按照数除尽这些质数剩下来的因数分组。
代码实现:开一个 v e c t o r vector vector数组, A [ i ] A[i] A[i]存放最后质数或者1为i的所有 s t a t e state state。同时将本身是平方数倍数的数去除。

void divide_factor(int x)
{
	int state = 0;
	for (int i=0; i<8; i++)
	{
		if (x % prime[i] == 0)
		{
			x /= prime[i];
			if (x % prime[i] == 0)
                return;
			state |= (1 << i);
		}
	}
	A[x].push_back(state);
}
  • 分组之后考虑: A [ 1 ] A[1] A[1]里的 s t a t e state state是任意取的,类似一个01背包。而 A [ i ] ( i &gt; 1 ) A[i](i&gt;1) A[i](i>1) s t a t e state state却不是任意取的,即每组只能取一个,是一个分组背包。突然放一个背包九讲链接,呵呵
  • 这里假设我们用三维的状态,即上述的 d p ( i , j , k ) dp(i,j,k) dp(i,j,k),我们写个代码:
memset(dp, 0, sizeof(dp));
dp[0][0][0] = 1;

for (int i=0; i<A[1].size(); i++)               //枚举物品
{
    int w = A[1][i];
    for (int j=0; j<=i; j++)                    //枚举到i最多可以取i个物品
        for (int s=0; s<(1<<8); s++)
        {
            dp[i+1][j][s] += dp[i][j][s];
            if ((s & w) == 0)
                {
                    dp[i+1][j+1][s|w] += dp[i][j][s];
                }
        }
}

如果不考虑内存大小的问题,这个代码会不会有问题?

问题出在这个 i i i上面,这个 i i i表示的并不是枚举到所有数的第 i i i个数,而是最后余下为1的数中的第 i i i个数,也就是说,对应每个 A [ x ] A[x] A[x]都可能有 i i i,所以这个 i i i会互相覆盖……

  • 那有没有什么办法避免这个问题?对 i i i进行处理,或者直接不要这个 i i i

01背包的一维数组就可以用啦,即逆序枚举 j j j,然后更新数组……

for (int i=0; i<A[1].size(); i++)
	{
		int w = A[1][i];
		for (int j=k-1; j>=0; j--)			//`w` can be taken for 1-k times
			for (int s=0; s<(1<<8); s++)	//enumerate the states
			{
			    if (dp[j][s])
				{
				    if ((s & w) == 0)
                        dp[j + 1][s | w] = (dp[j + 1][s | w] + dp[j][s]) % mod;
				}
			}
	}
  • 最后就是剩下 A [ x ] ( x &gt; 1 ) A[x](x&gt;1) A[x](x>1)的情况了,这里是分组背包。

f [ k ] [ v ] = m a x ( f [ k − 1 ] [ v ] , f [ k − 1 ] [ v − c [ i ] ] + w [ i ] ∣ 物 品 i 属 于 第 k 组 ) f[k][v]=max(f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组) f[k][v]=max(f[k1][v],f[k1][vc[i]]+w[i]ik)
一维数组伪代码:

for 所有的组k
    for v=V..0
        for 所有的i属于组k                 //这里是重点,把物品放到最内层枚举
            f[v]=max{f[v],f[v-c[i]]+w[i]}

然后我们套到这道题,感觉代码排版略丑……

for (int i=2; i<=n; i++)
	{
		if (A[i].size() > 0)
		{
			for (int j=k-1; j>=0; j--)
			{
				for (int s=0; s<(1<<8); s++)
				{
				    if (dp[j][s])
					{
					    for (int p=0; p<A[i].size(); p++)
                        {
                            int w = A[i][p];
                            if ((s & w) == 0)
                                dp[j + 1][s | w] = (dp[j + 1][s | w] + dp[j][s]) % mod;

                        }
					}
				}
			}
		}
	}
  • 然后我思考了一下为什么分组背包是把物品枚举放到里面,而01是放外面的。

假设一组里的状态为 w 1 , w 2 … w n w_1,w_2\dots w_n w1,w2wn,如果把这个循环放外面,可能存在情况某个状态 s = s ′ ∣ w 1 ∣ w 2 … ∣ w n s=s&#x27;|w_1|w_2\dots|w_n s=sw1w2wn的情况。但是如果放里面可以确保一个 s s s,肯定是 s ′ ∣ w 1 , s ′ ∣ w 2 , … , s ′ ∣ w n s&#x27;|w_1,s&#x27;|w_2,\dots,s&#x27;|w_n sw1,sw2,,swn的某一种。恩……我是这样感觉的……

  • 最后结果就是就是枚举选的个数和状态所有 d p [ j ] [ s ] dp[j][s] dp[j][s]的方案总数啦。

代码

hdu 6125 VJ

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;

typedef long long llong;
const int maxn = 500 + 10;
const int mod = 1e9 + 7;
int prime[8] = {2, 3, 5, 7, 11, 13, 17, 19};
vector <int> A[maxn];
int dp[maxn][(1<<8) + 100];
int n, k;

void divide_factor(int x)
{
    int state = 0;
    for (int i=0; i<8; i++)
    {
        if (x % prime[i] == 0)
        {
            x /= prime[i];
            if (x % prime[i] == 0)
                return;
            state |= (1 << i);
        }
    }
    //printf("x=%d state=%d\n", x, state);
    A[x].push_back(state);
}

void solve()
{
    memset(dp, 0, sizeof(dp));
    dp[0][0] = 1;

    for (int i=0; i<A[1].size(); i++)
    {
        int w = A[1][i];

        for (int j=k-1; j>=0; j--)            //w can be taken for once
            for (int s=0; s<(1<<8); s++)    //enumerate the status
            {
                if (dp[j][s])
                {
                    if ((s & w) == 0)
                        dp[j + 1][s | w] = (dp[j + 1][s | w] + dp[j][s]) % mod;
                }
            }
    }

    for (int i=2; i<=n; i++)
    {
        if (A[i].size() > 0)
        {
            for (int j=k-1; j>=0; j--)
            {
                for (int s=0; s<(1<<8); s++)
                {
                    if (dp[j][s])
                    {
                        for (int p=0; p<A[i].size(); p++)
                        {
                            int w = A[i][p];
                            if ((s & w) == 0)
                                dp[j + 1][s | w] = (dp[j + 1][s | w] + dp[j][s]) % mod;

                        }
                    }
                }
            }
        }
    }
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        scanf("%d %d", &n, &k);

        for (int i=1; i<=n; i++)
            A[i].clear();

        for (int i=1; i<=n; i++)
            divide_factor(i);

        solve();
        int ans = 0;
        for (int i=1; i<=k; i++)
            for (int j=0; j<(1<<8); j++)
                {
                    if (dp[i][j])
                        ans = (ans + dp[i][j]) % mod;
                }
        printf("%d\n", ans);
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: hdu 2829 Lawrence 斜率优化dp 这道题是一道经典的斜率优化dp题目,需要用到单调队列的思想。 题目大意是给定一个序列a,求出一个序列b,使得b[i]表示a[1]~a[i]中的最小值,且满足b[i] = min{b[j] + (i-j)*k},其中k为给定的常数。 我们可以将上式拆开,得到b[i] = min{b[j] - j*k} + i*k,即b[i] = i*k + min{b[j] - j*k},这个式子就是斜率优化dp的形式。 我们可以用单调队列来维护min{b[j] - j*k},具体思路如下: 1. 首先将第一个元素加入队列中。 2. 从第二个元素开始,我们需要将当前元素加入队列中,并且需要维护队列的单调性。 3. 维护单调性的方法是,我们从队列的末尾开始,将队列中所有大于当前元素的元素弹出,直到队列为空或者队列中最后一个元素小于当前元素为止。 4. 弹出元素的同时,我们需要计算它们对应的斜率,即(b[j]-j*k)/(j-i),并将这些斜率与当前元素的斜率比较,如果当前元素的斜率更小,则将当前元素加入队列中。 5. 最后队列中的第一个元素就是min{b[j] - j*k},我们将它加上i*k就得到了b[i]的值。 6. 重复以上步骤直到处理完所有元素。 具体实现可以参考下面的代码: ### 回答2: HDU 2829 Lawrence 斜率优化 DP 是一道经典的斜率优化 DP 题目,其思想是通过维护一个下凸包来优化 DP 算法。下面我们来具体分析一下这道题目。 首先,让我们看一下该题目的描述。题目给定一些木棒,要求我们将这些木棒割成一些给定长度,且要求每种长度的木棒的数量都是一样的,求最小的割枝次数。这是一个典型的背包问题,而且在此基础上还要求每种长度的木棒的数量相同,这就需要我们在状态设计上走一些弯路。 我们来看一下状态的定义。定义 $dp[i][j]$ 表示前 $i$ 个木棒中正好能割出 $j$ 根长度为 $c_i$ 的木棒的最小割枝次数。对于每个 $dp[i][j]$,我们可以分类讨论: 1. 不选当前的木棒,即 $dp[i][j]=dp[i-1][j]$; 2. 选当前的木棒,即 $dp[i][j-k]=dp[i-1][j-k]+k$,其中 $k$ 是 $j/c_i$ 的整数部分。 现在问题再次转化为我们需要在满足等量限制的情况下,求最小的割枝次数。可以看出,这是一个依赖于 $c_i$ 的限制。于是,我们可以通过斜率优化 DP 来解决这个问题。 我们来具体分析一下斜率优化 DP 算法的思路。我们首先来看一下动态规划的状态转移方程 $dp[i][j]=\min\{dp[i-1][k]+x_k(i,j)\}$。可以发现,$dp[i][j]$ 的最小值只与 $dp[i-1][k]$ 和 $x_k(i,j)$ 有关。其中,$x_k(i,j)$ 表示斜率,其值为 $dp[i-1][k]-k\times c_i+j\times c_i$。 接下来,我们需要维护一个下凸包,并通过斜率进行优化。我们具体分析一下该过程。假设我们当前要计算 $dp[i][j]$。首先,我们需要找到当前点 $(i,j)$ 在凸包上的位置,即斜率最小值的位置。然后,我们根据该位置的斜率计算 $dp[i][j]$ 的值。接下来,我们需要将当前点 $(i,j)$ 加入到下凸包上。 我们在加入点的时候需要注意几点。首先,我们需要将凸包中所有斜率比当前点小的点移除,直到该点能够加入到凸包中为止。其次,我们需要判断该点是否能够加入到凸包中。如果不能加入到凸包中,则直接舍弃。最后,我们需要保证凸包中斜率是单调递增的,这就需要在加入新的点之后进行上一步操作。 以上就是该题目的解题思路。需要注意的是,斜率优化 DP 算法并不是万能的,其使用情况需要根据具体的问题情况来确定。同时,该算法中需要维护一个下凸包,可能会增加一些算法的复杂度,建议和常规 DP 算法进行对比,选择最优的算法进行解题。 ### 回答3: 斜率优化DP是一种动态规划优化算法,其主要思路是通过对状态转移方程进行变形,提高算法的时间复杂度。HDU2829 Lawrence问题可以用斜率优化DP解决。 首先,我们需要了解原问题的含义。问题描述如下:有$n$个人在数轴上,第$i$个人的位置为$A_i$,每个人可以携带一定大小的行李,第$i$个人的行李重量为$B_i$,但是每个人只能帮助没有他们重量大的人搬行李。若第$i$个人搬运了第$j$个人的行李,那么第$i$个人会累加$C_{i,j}=\left|A_i-A_j\right|\cdot B_j$的体力消耗。求$m$个人帮助每个人搬运行李的最小体力消耗。 我们可以通过斜率优化DP解决这个问题。记$f_i$为到前$i$个人的最小体力消耗,那么状态转移方程为: $$f_i=\min_{j<i}\{f_j+abs(A_i-A_j)\cdot B_i\}$$ 如果直接使用该方程,时间复杂度为$O(n^2)$,如果$n=10^4$,则需要计算$10^8$次,运算时间极长。斜率优化DP通过一些数学推导将方程变形,将时间复杂度降低到$O(n)$,大大缩短了计算时间。 通过斜率优化DP的推导式子,我们可以得到转移方程为: $$f_i=\min_{j<i}\{f_j+slope(j,i)\}$$ 其中,$slope(j,i)$表示直线$j-i$的斜率。我们可以通过如下方式来求解$slope(j,i)$: $$slope(j,i)=\frac{f_i-f_j}{A_i-A_j}-B_i-B_j$$ 如果$slope(j,i)\leq slope(j,k)$,那么$j$一定不是最优,可以直接舍去,降低计算时间。该算法的时间复杂度为$O(n)$。 综上所述,斜率优化DP是一种动态规划优化算法,可以大大缩短计算时间。在处理类似HDU2829 Lawrence问题的时候,斜率优化DP可以很好地解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值