In mathematics you don't understand things, you just get used to them.

分类 笔记 下的文章

Description

Link.

给出 $N$ 个单词,每个单词有个非负权值 $C_{i}$,现要将它们分成连续的若干段,每段的代价为此段单词的权值和,还要加一个常数 $M$,即 $(\sum C_{i})^{2}+M$。现在想求出一种最优方案,使得总费用之和最小

Solution

设 $f_{i}$ 表示 $n=i$ 时的答案,转移方程为:

$$ f_{i}=\min\{f_{j}+S_{i..j}^{2}+M\} $$

显然过不了,推成可以斜率优化的形式。

我们假设决策 $j$ 优于决策 $k$ 且 $k<j<i$。

(一种错误的思路)

$$ f_{j}+S_{i..j}^{2}+M<f_{k}+S_{i..k}^{2}+M $$

$$ f_{j}+(S_{i}-S_{j})^{2}<f_{k}+(S_{i}-S_{k})^2 $$

$$ f_{j}+S_{i}^2-2S_{i}S_{j}+S_{j}^{2}<f_{k}+S_{i}^2-2S_{i}S_{k}+S_{k}^{2} $$

$$ f_{j}-2S_{i}S_{j}+S_{j}^{2}<f_{k}-2S_{i}S_{k}+S_{k}^{2} $$

$$ f_{j}-2S_{i}S_{j}+2S_{i}S_{k}+S_{j}^{2}-S_{k}^{2}-f_{k}<0 $$

$$ f_{j}-2S_{i}(S_{j}+ S_{k})+(S_{j}-S_{k})(S_{j}+S_{k})-f_{k}<0 $$

$$ f_{j}-(S_{j}+ S_{k})(2S_{i}+S_{j}-S_{k})-f_{k}<0 $$

$$ f_{j}-\frac{S_{j}+ S_{k}}{2S_{i}+S_{j}-S_{k}}-f_{k}<0 $$

最后发现做 $\text{TM}$ 不出来。

于是重新推:

(另一种错误思路)(我简直自闭)

$$ f_{j}+S_{i..j}^{2}+M<f_{k}+S_{i..k}^{2}+M $$

$$ f_{j}+(S_{i}-S_{j})^{2}<f_{k}+(S_{i}-S_{k})^2 $$

$$ f_{j}+S_{i}^2-2S_{i}S_{j}+S_{j}^{2}<f_{k}+S_{i}^2-2S_{i}S_{k}+S_{k}^{2} $$

$$ f_{j}-2S_{i}S_{j}+S_{j}^{2}<f_{k}-2S_{i}S_{k}+S_{k}^{2} $$

$$ f_{j}-f_{k}+S_{j}^{2}-S_{k}^{2}<2S_{i}S_{j}-2S_{i}S_{k} $$

$$ f_{j}-f_{k}+S_{j}^{2}-S_{k}^{2}<2S_{i}(S_{j}-S_{k}) $$

$$ f_{j}-f_{k}+(S_{j}-S_{k})(S_{j}+S_{k})<2S_{i}(S_{j}-S_{k}) $$

$$ \frac{f_{j}-f_{k}}{2(S_{j}-S_{k})}+\frac{S_{j}+S_{k}}{2}<S_{i} $$

嗯,还是 $\text{TM}$ 做不来。

好了,推倒重来。

(这次是对的)

$$ f_{i}=\min\{f_{j}+S_{i..j}^{2}+M\} $$

$$ f_{j}+S_{i..j}^{2}+M<f_{k}+S_{i..k}^{2}+M $$

$$ f_{j}+(S_{i}-S_{j})^{2}<f_{k}+(S_{i}-S_{k})^2 $$

$$ f_{j}+S_{i}^2-2S_{i}S_{j}+S_{j}^{2}<f_{k}+S_{i}^2-2S_{i}S_{k}+S_{k}^{2} $$

$$ f_{j}-2S_{i}S_{j}+S_{j}^{2}<f_{k}-2S_{i}S_{k}+S_{k}^{2} $$

$$ f_{j}-f_{k}+S_{j}^{2}-S_{k}^{2}<2S_{i}S_{j}-2S_{i}S_{k} $$

$$ f_{j}-f_{k}+S_{j}^{2}-S_{k}^{2}<2S_{i}(S_{j}-S_{k}) $$

$$ \frac{(f_{j}+S_{j}^{2})-(f_{k}+S_{k}^{2})}{2(S_{j}-S_{k})}<S_{i} $$

我们令 $Y_{i}=f_{i}+S_{i}^{2}$,$X_{i}=2S_{i}$,则我们有:

$$ \frac{Y_{j}-Y_{k}}{X_{j}-X_{k}}<S_{i} $$

挺好的,正好就是斜率优化的形式!

斜率优化!

牛逼啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎哎啊啊哎。

那么接下来我们设 $F(j,k)=\frac{Y_{j}-Y_{k}}{X_{j}-X_{k}}$。

其中如果满足 $F(j,k)<S_{i}$,则我们称决策 $j$ 优于决策 $k$。

现在我们继续设 $k<j<i$,那么如果满足 $F(i,j)<F(j,k)$,决策 $j$ 就永远不可能成为最优解。

证明不会,$\text{PPT}$ 没看懂。

最后放个 $\text{PPT}$ 里写的 $\text{Summary}$:

1、用一个单调队列来维护解集。

2、假设队列中从头到尾已经有元素 $a$,$b$,$c$。那么当 $d$ 要入队的时候,维护队列的上凸性质,即如果 $F(d,c)<F(c,b)$,那么删除点 $c$。直到找到 $F(d,x)\ge F(x,y)$ 为止,并将 $d$ 点加入在该位置中。

3、求解的时候,从对头开始,如果已有元素 $a$,$b$,$c$,当 $i$ 点要求解时,如果 $F(b,a)<S_{i}$,说明 $b$ 点比 $a$ 点更优,$a$ 点直接排除,于是 $a$ 出队,最后 $f_{i} = \mathrm{getDp}(Q[\mathrm{head}])$。

#include <cstdio>
#include <cstdlib>
using namespace std;
typedef long long LL;

namespace MySpace
{
    template<class _T>
    void read(_T &x)
    {
        x = 0;
        char c = getchar();
        _T f = 1;
        while( c < '0' || c > '9' )
        {
            if( c == '-' )    f = -1;
            if( c == -1 )    exit( 0 );
            c = getchar();
        }
        while( c >= '0' && c <= '9' )    x = x * 10 + c - '0', c = getchar();
        x *= f;
    }
    
    template<class _T>
    void write(_T x)
    {
        if( x < 0 )    putchar( '-' ), x = -x;
        if( x > 9 )    write( x / 10 );
        putchar( x % 10 + '0' );
    }
    
    const LL MAXN = 5e5 + 5;
    LL N, M, Dp[MAXN], Q[MAXN], S[MAXN], A[MAXN];
    
    LL Square( LL x ) { return x * x; }
    LL Up( LL i ) { return Dp[i] + Square( S[i] ); }
    LL Down( LL i ) { return S[i] << 1; }
    LL FracUp( LL j, LL k ) { return Up( j ) - Up( k ); }
    LL FracDown( LL j, LL k ) { return Down( j ) - Down( k ); }
}

int main()
{
    using namespace MySpace;
    while( ~ scanf( "%lld%lld", &N, &M ) )
    {
        for( LL i = 1; i <= N; ++ i )    scanf( "%lld", &A[i] ), S[i] = S[i - 1] + A[i];
        LL l = 1, r = 1;
        Q[r] = 0;
        for( LL i = 1; i <= N; ++ i )
        {
            while( l < r && FracUp( Q[l + 1], Q[l] ) < S[i] * FracDown( Q[l + 1], Q[l] ) )    l ++;
            Dp[i] = Dp[Q[l]] + Square( S[i] - S[Q[l]] ) + M;
            while( l < r && FracUp( i, Q[r] ) * FracDown( Q[r], Q[r - 1] ) <= FracUp( Q[r], Q[r - 1] ) * FracDown( i, Q[r] ))    r --;
            Q[++ r] = i;
        }
        printf( "%lld\n", Dp[N] );
    }
//    while( read( N ), read( M ), 1 )
//    {
//        for( LL i = 1; i <= N; ++ i )    read( A[i] ), S[i] = S[i - 1] + A[i];
//        LL l = 1, r = 0;
//        Q[l] = 0;
//        for( LL i = 1; i <= N; ++ i )
//        {
//            while( l <= r && FracUp( Q[l + 1], Q[l] ) < S[i] * FracDown( Q[l + 1], Q[l] ) )    l ++;
//            Dp[i] = Dp[Q[l]] + Square( S[i] - S[Q[l]] ) + M;
//            while( l <= r && FracUp( i, Q[r - 1] ) * FracDown( Q[r - 1], Q[r - 2] ) < FracUp( Q[r - 1], Q[r - 2] ) * FracDown( i, Q[r - 1] ))    r ++;
//            Q[++ r] = i;
//        }
//        write( Dp[N] ), putchar( '\n' );
//    }
    return 0;
}

Description

Link.

给一个数列和 $m$,在数列任选若干个数,使得他们的和对 $m$ 取模后最大。

Solution

记录一下犯下的一个 nt 错误。

首先我们有一个显然的 DFS 暴力,每次两种决策,选或不选,所以时间复杂度为 $\Theta(2^{n})$。

$n$ 的范围是 35,是过不了的,我们可以考虑折半搜索。

关于折半搜索可以看看 我的折半搜索小计

暴力搜出 $[1,\lfloor\frac{n}{2}\rfloor],[\lfloor\frac{n}{2}\rfloor+1,n]$ 的所有答案,记录到两个 vector 里面。

这一部分的时间复杂度是 $\Theta(2^{\lfloor\frac{n}{2}\rfloor})$。

考虑合并贡献。

先考虑一个暴力合并贡献的方法。

我们记第一次搜索搜出来的答案序列为 $A_{1}$,同理有 $A_{2}$。

这里的两个答案序列都是在模 $m$ 意义下的。

那么对于每一个 $A_{1,i}$,我们都可以暴力在 $A_{2}$ 中寻找两者相加模 $m$ 的最大值。

那么我们可以分类讨论了,因为序列在模 $m$ 意义下,所以我们对于每一个 $A_{1,i}$ 找到的 $A_{2,j}$ 使得 $(A_{1,i}+A_{2,j})\bmod m$ 最大,都只有两种情况。

一种是 $A_{2,j}$ 在 $A_{2}$ 中值域范围在 $[0,m-A_{1,i}-1]$ 的所有值中最大,一种是在 $A_{2}$ 中值域范围在 $[0,m\times2-A_{1,i}-1]$ 的所有值中最大。

所以我们在这两种情况中取最大即可。

由于我不理解二分做法,所以我用的是动态开点值域线段树。

(flag:动态开点不加引用就【】)

#include<bits/stdc++.h>
using namespace std;
const int N=35+5;
const int H=99999999;
int n,m,tot=1,root=1,ans,a[N];
struct Tree
{
    int ls,rs,val;
} nodes[(1<<(N>>1))<<3];
vector<int> vec[2];

void dfs(int x,int cur,int lim)
{
    if(x>lim)
    {
        if(lim==(n>>1))     vec[0].push_back(cur);
        else    vec[1].push_back(cur);
        return ;
    }
    dfs(x+1,(cur+a[x])%m,lim);
    dfs(x+1,cur,lim);
}

void ins(int &p,int l,int r,int x)
{
    if(p==0)    p=++tot;
    if(l==r)
    {
        nodes[p].val=l;
        return ;
    }
    int mid=(l+r)>>1;
    if(mid>=x)    ins(nodes[p].ls,l,mid,x);
    else    ins(nodes[p].rs,mid+1,r,x);
    nodes[p].val=max(nodes[nodes[p].ls].val,nodes[nodes[p].rs].val);
}

int find(int p,int l,int r,int x,int y)
{
    if(l>y||r<x)    return 0;
    if(l>=x&&r<=y)    return nodes[p].val;
    int mid=(l+r)>>1,ret=0;
    if(mid>=x)    ret=max(ret,find(nodes[p].ls,l,mid,x,y));
    if(mid<y)    ret=max(ret,find(nodes[p].rs,mid+1,r,x,y));
    return ret;
}

void output(int p)
{
    if(nodes[p].ls==0&&nodes[p].rs==0)
    {
        printf("%d ",nodes[p].val);
        return ;
    }
    output(nodes[p].ls);
    output(nodes[p].rs);
}

signed main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;++i)    scanf("%d",&a[i]);
    dfs(1,0,n>>1);
    dfs((n>>1)+1,0,n);
    sort(vec[0].begin(),vec[0].end());
    sort(vec[1].begin(),vec[1].end());
    for(auto x:vec[1])    ins(root,0,m-1,x);
    for(auto x:vec[0])    ans=max(ans,max(x+find(1,0,m-1,0,m-x-1),(x+find(1,0,m-1,0,m*2-x-1))%m));
    printf("%d\n",ans);
    return 0;
}

Description

Link.

一完全图有 $n$ 个节点 $0,...,n-1$,其中边 $(i,j)$ 的权值为 $i\oplus j$,其中 $\oplus$ 为位异或操作,试求出最小生成树的边权和。

Solution

先从递推的层面考虑.

我们定义 $F(n)$ 表示结点数为 $n$ 的答案,也就是最小生成树的边权和.

首先边界条件为 $F(0)=0,F(1)=1$.

然后我们考虑如何从 $F(n-1)$ 推到 $F(n)$.

每当我们新加入一个结点 $n-1$(题目结点编号从 0 开始),它的点权为其本身,也就是 $n-1$,那么此时我们就要从之前的 $n-1$ 个结点中选出一个点与 $n-1$ 相连构成当前的最小生成树.

因为边 $(u,v)$ 的边权 $w(u,v)=u\ \mathrm{xor}\ v$ 且图为完全图,所以我们每加入一个新结点 $n-1$ 时,所有我们之前的 $0\cdots n-2$ 号结点都可以被选择.

那么问题转化为:对于一个数 $n-1$,我们需要选出一个整数 $x\in[0,n-1)$ 使得 $(n-1)\ \mathrm{xor}\ x$ 最小.

考虑异或运算的定义:每一位相同为零,不同为一.

那么我们选出的 $x$,需要满足二进制意义下每一位和 $n-1$ 尽量相同,并且从右到左(也就是二进位从低到高)的第一个不同的位置尽量低.

那么结论就摆在眼前了,我们选择的这个 $x$ 为 $(n-1)-\mathrm{lowbit}(n-1)$.

为什么?想想 $\mathrm{lowbit(x)}$ 操作的定义:二进制下 $x$ 最低的 1 和后面的 0 组成的二进制数.

这样结论的正确性就显然了.

我们 $F(n)$ 的递推公式为 $F(n)=F(n-1)+(n\ \mathrm{xor}\ (n\ \mathrm{xor}\ \mathrm{lowbit}(n)))$.

那么暴力递推的代码如下:

(code?)

#include<bits/stdc++.h>
using namespace std;
long long f[100005];
signed main()
{
    long long n;
    scanf("%lld",&n);
    f[0]=0;
    f[1]=1;
    for(long long i=2;i<n;++i)   f[i]=f[i-1]+(i^(i^(i&-i)));
    printf("%lld\n",f[n-1]);
    return 0;
}

仔细观察一下递推式,$n\ \mathrm{xor}\ (n\ \mathrm{xor}\ \mathrm{lowbit}(n))$ 不就是 $\mathrm{lowbit}(n)$ 嘛!

那么为题转化为求 $\mathrm{lowbit}$ 前缀和.

通过打一个 $\mathrm{lowbit}$ 表的方法,我们发现 $\mathrm{lowbit}$ 的值十分有规律,就像这种形式:

$$ \texttt{1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 16 1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 32}\cdots $$

其实这种规律要证明也很方便,只要根据二进制数末尾的情况即可得知.

虽然这个规律没啥用,但是启发了我们按位统计贡献的方法在 $\Theta(1)$ 空间 $\Theta(\log_{2}n)$ 的时间内计算出了 $\mathrm{lowbit}$ 前缀和.

具体方法请参考代码.

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
signed main()
{
    LL n;
    scanf("%lld",&n);
    LL ans=0,app=1,low=n;
    while(low>1)  ans+=app*(low>>1),low-=(low>>1),app<<=1;
    printf("%lld\n",ans);
    return 0;
}