2024.2.4+2024.2.5

· · 个人记录

DP

前置

最难点在于看出来是dp

(易与网络流混淆)

状态,转移,初始化

状态最基础(关键)

转移:状态之间的关系(简单)

初始化:状态的边界条件

设计状态:找到题目中所有在变化的量,将其作为状态(先塞再删)

不能用状态以外的信息

eg:数字三角形上,求走到最后%100最大为多少

[tyvj-1076] - 数字三角形2

不同于数字三角形原题,目前最优不保证一直最优

(所有dp的错误都来自状态不够)

所以再加一维状态

f_{i,j,k}

表示在 ( i , j ) 位置上,模数为 k 是否可能

转移方式

用别人求自己

用自己求别人

(博弈论dp)记忆化搜索(不好for的时候)

分类

有模版:背包,区间,树形,数位,状压,插头(不考),排列,博弈论

一般dp(更难)

eg:Paint Pearls

$n\sqrt{n}$ dp(相同颜色的区间越长越好,最大代价为n,则最多$\sqrt{n}$种不同颜色,双指针处理区间) ## 排列dp $n!$ 种排列中,满足要求的有多少 ### 转移方法 把所有数从小到大或从大到小插入的过程 $f_i$ 表示 $i$ 已经被插入 枚举插入的位置,计算方案 ***eg*** $[1,n]$ 所有的排列中,逆序对数为偶数的数量 $f_{i,j}$表示,$[1,i]$已经被插入,当前j个逆序对的方案数 第i+1个数,插入k位置,会与其后的所有数形成逆序对 $f_{1,0}=1
cin>>n;
f[1][0]=1;
for(int i=1;i<=n;i++)
{
    for(int j=0;j<=i*(i-1)/2;j++)//逆序对数最多为每个数都 与后面的数 形成逆序对的情况 
    {
        for(int k=0;k<=i;k++)
        {
            f[i+1][j+i-k]+=f[i][j];
        }
    }
}

针对本题优化: j:0/1

 cin>>n;
f[1][0]=1;
for(int i=1;i<=n;i++)
{
    for(int j=0;j<=1;j++)
    {
        for(int k=0;k<=i;k++)
        {
            f[i+1][(j+i-k)&1]+=f[i][j];
        }
    }
}

可优化到O(n)

O(1)

[***eg*** ](https://www.codechef.com/problems/LEMOVIE) **或[Little Elephant and Movies](https://vjudge.net/problem/CodeChef-LEMOVIE)** $f_{i,j}$ 表示 $[i,n]$插入,激动值为j的方案数 插入到前面,激动值+1; 插入到后面,激动值不变 ## 背包 ### 01背包 [01背包 采药](https://www.luogu.com.cn/problem/P1048) $f_{i,j}$ 前 $i$ 个物品考虑完毕,用了 $j$ 的体积的最大价值 $f_{i,j}=max(f_{i-1,j},f_{i-1,j-v_i}+w_i)

完全背包

状态一样,枚举个数

优化

由于不关心到底选了多少个

所以f_{i,j} 不从 f_{i-1,j-v_i} 转移,从 f_{i,j-v_i} 转移

将斜线拆为一竖一横

for(int i=1;i<=n;i--)
{
    for(int j=0;j<m;j++)
        {
        f[i][j]=f[i-1][j];
        if(j>=v[i])
        {
            f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
        }
    }
}

多重背包

每个物品有限个

(想办法将未知变为已知)

(可能的出新题方法:TA+TB=TC)

将物品个数进行二进制拆分

eg: 17=1+2+4+8+2

转化为01背包

复杂度:O(nm\log_2{n})

for(int i=1;i<=cnt;i++)
{
    int tiji,jiazhi,geshu;
    cin>>tiji>>jiazhi>>geshu;
    int k=1;
    while(k<=geshu)
    {
        n++;
        v[n]=tiji*k;
        w[n]=jiazhi*k;
        geshu-=k;
        k*=2;
    }
    if(geshu!=0)
    {
        n++;
        v[n]=tiji*geshu;
        w[n]=jiazhi*geshu;
    }
}

P4095 [HEOI2013] Eden 的新背包问题

正着跑一遍,倒着跑一遍,每次不算这个物品的区间,将两个区间拼起来

数位dp

eg给出l,r,求出[l,r]有几个数(不能 r-l+1 )

[l,r]=[0,r]-[0,l-1]

转移方法

从高位开始,一位一位填数,枚举下一位填什么

状态

```c++ int calc(int z)//计算0~z所有数的数位之和 { if(z==0) { return 1; } int n=0; while(z) { n++; x[n]=z%10; z/=10; } memset(f,0,sizeof(f));//转化为前缀和,因此要清空 f[n+1][1]=1;//只有一种方法,填 0 for(int i=n;i>=1;i--)//当前要填y[i]的值,此时已经填了y[i+1~n] { for(int j=0;j<=1;j++)//填y[i]之前,是<还是= { int up=9;//能填的最大值 if(j==1) { up=x[i]; } for(int k=0;k<=up;k++) { f[i][(j==1)&&(k==up)]+=f[i+1][j]; } } } return f[1][0]+f[1][1]; } ``` ***eg*** 计算 $[l,r]$ 的数位和 ```c++ int calc(int z)//计算0~z所有数的数位之和 { if(z==0) { return 1; } int n=0; while(z) { n++; x[n]=z%10; z/=10; } memset(f,0,sizeof(f));//转化为前缀和,因此要清空 f[n+1][1]=1;//只有一种方法,填 0 for(int i=n;i>=1;i--)//当前要填y[i]的值,此时已经填了y[i+1~n] { for(int j=0;j<=1;j++)//填y[i]之前,是<还是= { int up=9;//能填的最大值 if(j==1) { up=x[i]; } for(int k=0;k<=up;k++) { f[i][(j==1)&&(k==up)]+=f[i+1][j]; w[i][(j==1)&&(k==up)]+=f[i+1]*k+w[i+1][j]; } } } return w[1][0]+w[1][1]; } ``` [windy数](https://www.luogu.com.cn/problem/P2657) ```c++ #include<bits/stdc++.h> #define endl '\n' #define int long long using namespace std; int a,b; int f[15][2][15]; int x[15]; int calc(int z) { if(z==0) { return 0; } int n=0; memset(x,0,sizeof(x)); while(z) { n++; x[n]=z%10; z/=10; } memset(f,0,sizeof(f)); for(int i=1;i<=x[n];i++) { f[n][(i==x[n])][i]++; } for(int i=n-1;i>=1;i--)//填好所有最高位的可能情况,防止前导0; { for(int j=1;j<=9;j++) { f[i][0][j]++; } } for(int i=n;i>=1;i--) { for(int j=0;j<=1;j++) { for(int k=0;k<=9;k++) { int up=9; if(j) { up=x[i]; } for(int l=0;l<=up;l++) { if(abs(k-l)>=2) { f[i][(j==1)&&(l==up)][l]+=f[i+1][j][k]; } } } } } int res=0; for(int j=0;j<=1;j++) { for(int k=0;k<=9;k++) { res+=f[1][j][k]; } } return res; } signed main() { cin>>a>>b; int ans=calc(b)-calc(a-1); cout<<ans; return 0; } ``` [Blinker 的仰慕者](https://www.luogu.com.cn/problem/P5842) $f_{i,j,k}$ 分别表示位数,是否有当前位限制,积为k的方案数 显然要炸 由于$ 10 $以内的数质因数分解只有2,3,5,7 $f_{i,0/1,a,b,c,d}$分别表示位数,是否有限制,积为 $2^a\times3^b\times5^c\times7^d$ 的方案数 但是还是过不了 但是 $a,b,c,d$ 不能同时取最大,肯定有大量不合法的 将合法的 $2^a\times3^b\times5^c\times7^d$ 存在z数组中(大约3万个) 将4维压回一维 这样就能过了 ## 区间dp [石子合并(弱化版)](https://www.luogu.com.cn/problem/P1775) [石子合并](https://www.luogu.com.cn/problem/P1880) ### 特征(第一类区间dp,不绝对) 合并,相邻 ### 方法 枚举分界点 (注意,按区间大小从小到大转移,先长度,再区间,否则就爆了) ### 第二类 ***eg*** 给定字符串,求回文子序列的数量(HDU4632) $f_{l,r}=f_{l+1,r}+f_{l,r-1}-f_{l+1,r-1} if( s_l == s_r ) f_{l,r}+=f_{l+1,r-1}

方法

扔掉左端点和右端点

关键词(不绝对)

子序列

树形dp

在树上做的dp

eg 给定n个点的树,求这棵树有多少点

### 转移 把所有儿子的信息整合 ***eg*** 给定一棵n个点的树,每边有长度,$dist(i,j)$ 表示i到j的路径长度,求$\sum\limits_{i=1}^{n}\sum\limits_{j=1}^{n}dist(i,j)

暴力求肯定炸

我们只关心一条边经过了几次

将内外的点数相乘,全部加起来 答案再乘2; ($i,j$ 都是1~n循环,$(i,j)$ 一次,$(j,i)$ 一次) ***eg*** 求$max dist(i,j)

求最长路,次长路即可

vector<Node> G[N];
int f[N];//最长 
int g[N];//次长距离 
void dfs(int u,int fa)
{
    f[u]=0;
    g[u]=0;
    for(auto x:G[u])
    {
        v=x.v;
        w=x.w;
        if(v==fa)
        {
            continue;
        }
        dfs(v,u);
        int dis=f[v]+w;
        if(dis>f[u])
        {
            g[u]=f[u];
            f[u]=dis;
        }
        else if(dis>g[u])
        {
            g[u]=dis;
        }
    }
}

eg 求最大独立集

最大独立集:选出尽量多的点,使得点与点之间不相邻

P1352 没有上司的舞会

#include <bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N=6e3+5;
short r[N];
int f[N][2];
int n;
vector<int> g[N];
int ind[N];
int s;
void dfs(int u,int fa)
{
    f[u][1]=r[u];
    f[u][0]=0;
    for(auto v:g[u])
    {
        if(v==fa)
        {
            continue;
        }
        dfs(v,u);
        f[u][0]+=max(f[v][0],f[v][1]);
        //u不来,v可来可不来 
        f[u][1]+=f[v][0];
        //u来,v一定不来 
    }
}
signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>r[i];
    }
    for(int i=1;i<n;i++)
    {
        int x,y;
        cin>>x>>y;
        g[y].push_back(x);
        ind[x]++;
    }
    for(int i=1;i<=n;i++)
    {
        if(ind[i]==0)
        {
            s=i;
            break;
        }
    }
    dfs(s,0);
    cout<<max(f[s][1],f[s][0]);
    return 0;
}

保安站岗

#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int N=1510;
const int INF=0x3f3f3f3f;
int n;
int r[N];
int m;
int f[N][3];
//f[i][0] 表示 该节点不选 孩子看 
//f[i][1] 表示 该节点选 
//f[i][2] 表示 该节点不选 父亲看 
vector<int > g[N];
void dfs(int u, int fa)
{
    f[u][1]=r[u];
    bool flag=0;
    int tmp = INF;
    for(auto v : g[u])
    {
        if(v == fa)
        {
            continue;
        }
        dfs(v, u);
        f[u][1] += min({f[v][0], f[v][1], f[v][2]});
        f[u][2] += min(f[v][0], f[v][1]);
        if(f[v][1] <= f[v][0])
        {
            flag=1;
        }
        else
        {
            tmp = min(tmp, f[v][1] - f[v][0]);
        }
        f[u][0] += min(f[v][0], f[v][1]);
    }
    if(flag == false)
    {
        f[u][0] += tmp;
    }
   //f[u][0]也可通过辅助数组记录前i个儿子中是否有保安的情况最小花费
}

signed main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        cin>>r[x];
        cin>>m;
        for(int j=1;j<=m;j++)
        {
            int y;
            cin>>y;
            g[x].push_back(y);
            g[y].push_back(x);
        }
    }
    dfs(1,0);
    cout<<min(f[1][0],f[1][1]);
    return 0;
}

状压dp

eg 旅行商问题

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int inf=0x3f3f3f3f;
int n;
double f[1<<20][20];//f[s][i] 当前在i点 s代表那些点被走过 
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>x[i]>>y[i];
    }
    for(int i=0;i<(1<<n);i++)
    {
        for(int j=0;j<n;j++)
        {
            f[i][j]=1e20;
        }
    }
    f[1][0]=0;
    for(int i=0;i<(1<<n);i++)
    {
        for(int j=0;j<n;j++)
        {
            if(i&(1<<j))
            {
                for(int k=0;k<n;k++)
                {
                    if((1&(1<<k))==0)
                    {
                        f[i|(1<<k)][k]=min(f[i|(1<<k)][k],f[i][j]+dis(j,k));
                    }
                }
            }
        }
    }
    double ans=1e20;
    for(int i=0;i<n;i++)
    {
        ans=min(ans,f[(1<<n)-1][i]);
    }
    cout<<ans;
    return 0;
}

特征

n不大

egCorn Fields G

***eg***[互不侵犯](https://www.luogu.com.cn/problem/P1896) 改状态,加一维表示国王数量 ## 一般dp [Little Elephant and Mouses](https://vjudge.net/problem/CodeChef-LEMOUSE) [中国象棋](https://www.luogu.com.cn/problem/P2051) ## 优化 ***eg*** 从一号点出发,走 $k$ 步到 $n$ 点的方案数 $m[i][j]=1$ 表示 $i,j$ 之间有路 $ f_{i,j}=\sum\limits_{k=1}^{n} f_{i-1,k}\times m_{k,j} f_{i,1,j}=\sum\limits_{k=1}^{n} f_{i-1,1,k}\times m_{k,j} f_i[1][j]=\sum\limits_{k=1}^{n} f_{i-1}[1][k]\times m[k][j] f_t=f_{t-1}\times m f_t=f_0\times m^{t}

矩阵快速幂

特征

1.f_{i} 只和 f_{i-1} 有关

转移系数 m i 无关

***eg*** [迷路](https://www.luogu.com.cn/problem/P4159) 拆边,将其拆为边权为$1

但是本题为有向图,还有自环,最多拆成共有n+8n^2 个点

但是有一些点可以重复利用 可以每个点连一条链,每当有原来的点相连,就向外拐出 最多$90$ 个点 这样就转化为上一题了 ***eg*** 题意同$T1$ 每个点 $r$ 个入度 $ r $ 个出度 $in_{i,j}$ 表示$i$ 点的第$j$个出度 $out_{i,j}$ 同理 $m_{i,j}$ : 从$ i$到$j$ 有几条边 $m_{i,j}=\sum\limits _{k=1}^{r} out_{i,k}\times in_{j,k}

(n<=10^3,r<=20,t<=10^9)

r入手

观察m ,发现如果将in的两维存反,m=in\times out

m^t=(out\times in)^t = out\times (in\times out)^{t-1}\times in $in$ 是 $r\times n$ 的矩阵(存反后) $out$ 是 $n\times r$ 的矩阵 所以$in\times out$ 是$r\times r$的矩阵 将$n^3\times log_2{t}$ 复杂度转化为 $r^3\times log_2{t}

博弈论dp

eg A,B两人,有数n,两人有4种操作,将n 减1,2,4,8

$f[-7]$到 $f[0]$ 为true 所有能转移到的状态如果有false,那么自己必胜 如果全为true 那么自己必败 ```c++ bool f[N];//f[i]表示数为i时必胜还是必败 bool g[N];//g[i]代表f[i]求没求过 int a[]={1,2,4,8}; bool dfs(int i) { if(i<=0) { return true; } if(g[i]) { return f[i]; } g[i]=1; f[i]=0; for(int j=0;j<4;j++) { if(!dfs(i-a[j])) { f[i]=1; } } return f[i]; } ``` ### 总结(第一类) 状态为游戏状态 如果能转移到必败态,那么必胜 如果不能,那么必败 [Sereja and Game](https://vjudge.net/problem/CodeChef-SEAGM) ***eg*** 有$a$ 数组,不能把$a_i$ 变的$<=0$,谁无法操作,即为输 (每次$-1,-2,-4,-8$) 定义$sg$值 - 必败态的$sg$值为$0

sg定理

sg[a_1] \oplus sg[a_2] \oplus sg[a_3]......^sg[a_n]

特征(第二类)

多个游戏

做法

经典例题 Nim石子游戏问题

$sg[0]=0; sg[1]=1; sg[2]=2 ...

n 堆石子数异或起来

番外

“我们今天不讲(插头dp),又不考,(插头dp)如果出了就尽情骂出题人就行了,不用克制,不用收敛”

英文题面读不懂怎么办? fanyi.baidu.com

比今年,啊不,去年noipT4简单,今年题是啥我还不知道,我也不知道我还出不出题,我已经有几年没出题了

"同学们私聊我问问题的时候注意一下是不是开了陌生人不能发消息"

"种国王"

"时间和空间都比较炸裂"