poj3590 The shuffle Problem(置换+dp)

题目链接

题目大意:一个置换多次操作后就可以回到最初的状态,这个次数称为置换的循环节。求长度为n的序列的最大的循环节x,并且构造循环节为x的字典序最小的方案。

分析:
这道题是bzoj1025的变式

如果我们已知一个置换
我们通过找到ta的轮换就可以计算出ta的循环节: lcm(size) l c m ( s i z e 轮 换 )
我们要使长度为n的置换的循环节尽可能长
就需要产生一种分割方式,把序列分成若干端(若干轮换),使得所有部分长度的lcm最大

上述问题是可以用dp解决的

已知循环节以及分割方式,我们就可以构造解了
直接贪心的把分割长度从小到大放到原始的递增序列上,错一位即可

example:
      a:  1 2 3 4 5
分割方式: 1 2|3 4 5
    ans:  2 1 4 5 3

下面我们就讨论一下dp的过程:

一开始我想的比较简单: f[i] f [ i ] 表示数 i i 的最大分割方案
f[i]=max(f[i],lcm(f[j],ij))
再开一个数组 g g 记录每个状态的转移点,最后就可以倒退得到最大分割方案了

但是为什么WA了呢?
这样dp虽然可以计算出正确的循环节长度
但是得到的分割方案有可能段数较少,长度较大,得到的解不是字典序最小

实际上,题目可以转化为:

给你一个整数n,求a1+a2+a3...+am=n, 并且 a1,a2,...,am a 1 , a 2 , . . . , a m 的最小公倍数最大
我们要保证求得最小公倍数之后置换排序最小

考虑 lcm l c m 最简单的定义:
lcm(a,b)=pk11pk22pk33...pkmm l c m ( a , b ) = p 1 k 1 ∗ p 2 k 2 ∗ p 3 k 3 ∗ . . . ∗ p m k m
其中 pi p i 是指数, ki=max(kai,kbi) k i = m a x ( k a i , k b i )
也就是说我们将lcm质因数分解了
pkii p i k i 都是互素的,所以我们可以把序列分割成长度为 pkii p i k i 的轮换,同时保证 pkii=n ∑ p i k i = n

设计状态: f[i][j] f [ i ] [ j ] 表示使用了前 i i 个素数,当前pk和为 j j 的lcm最大值
(100以内一共有25个素数;pk实际上就是一个轮换的长度,那么 j j 就是序列的长度了)
转移:f[i][j]=max(f[i1][j],f[i1][k]prime[i]jk)
用另一个数组记录每一个状态的转移点

这样我们就可以把lcm分解成长度分别是为 pk11pk22pk33...pkmm p 1 k 1 , p 2 k 2 , p 3 k 3 , . . . , p m k m 的若干轮换

注意

这样就转化成了一个特殊的分组背包
质数 pi,p2i,p3i,...,pni p i , p i 2 , p i 3 , . . . , p i n 当做同组的物品,
对于同组的物品只能选一个或者都不选,总和为 n n 的最大值

for 所有的组k
    for v=V..0
        for 所有的i属于组k
            f[v]=max{f[v],f[v-c[i]]+w[i]}

tip

在代码中,如果我们不选一个数:f[i][j]=f[i-1][j];g[i][j]=0;
虽然我们可以在f[tot][n]处的到最大循环节
但是有些素数是我们没有用上的,
如果g[i][j]=0,就说明这个数没有用

我们在构造解的时候,可能会发现:用的数的 pkii p i k i 之和不到n,
这时我们就用长度为1的轮换补充即可

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;

const int N=102;
int sshu[26]={0,2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,57,61,67,71,73,79,83,89,97};
int n,f[30][N],g[30][N],a[N];

void solve()
{
    memset(f,0,sizeof(f)); f[0][0]=1;
    memset(g,0,sizeof(g));
    for (int i=1;i<=25;i++)
        for (int j=n;j>=0;j--)       //倒序 
        {
            f[i][j]=f[i-1][j];       //不用这个素数
            int now=sshu[i];
            while (j>=now)
            {
                if (f[i][j]<f[i-1][j-now]*now)
                    f[i][j]=f[i-1][j-now]*now,g[i][j]=now;   //now 这个轮换的长度 
                now*=sshu[i];
            }
        } 

    int ans=0,t=0;
    for (int i=n;i>=0;i--) 
        if (f[25][i]>ans) ans=f[25][i],t=i;
    printf("%d ",ans);
    int cnt=0,x=25,one,tt=0;
    while (t)
    {
        if (!g[x][t]) x--;
        else a[++cnt]=g[x][t],t-=g[x][t],x--,tt+=a[cnt];    
    } 
    one=n-tt;    //长度为1的轮换

    sort(a+1,a+1+cnt);
    for (int i=1;i<=one;i++) printf("%d ",i);
    int s=one+1;
    for (int i=1;i<=cnt;i++)
    {
        for (int j=1;j<a[i];j++) printf("%d ",j+s);
        printf("%d ",s);
        s+=a[i];
    }
    printf("\n");
}

int main()
{
    int T;
    scanf("%d",&T);
    while (T--)
    {
        scanf("%d",&n);
        solve();
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值