dp第三弹~数位统计类dp,状压dp,树形dp

计数dp

dp最难的就是想出来状态表示和分情况讨论

计数问题
类似小学数奥问题,最重要的就是分情况讨论

我们这里首先实现一个count(n, x)函数,这个函数的作用就是,求出来1到n中x出现的次数,一般x是0~9
对于本题,答案就是count(b, x)- count(a-1, x)

举个例子,1~n, x = 1
然后有abcdefg七位,求出来1在每一位上出现的次数,然后累加就是总次数dp第三弹~数位统计类dp,状压dp,树形dp_第1张图片
解释一下上图
1 <= abc1efg <= abcdefg
分成两种情况,
一种是abc任取 000~abc-1 这时候efg可任取,由于一直d是x(这里就是1) 然后这时候数量就是abc*1000:理解为abc种,每一种有1000种
第二种就是abc就 等于abc,这时候再次细分成三种情况,
1.一种是d < x(这里为1) 这时候,显然不可能存在可取的情况
2.d ==x, 这时候efg可取000~efg 一共efg+1种
3.d > x 这时候efg可任取000~999共1000种情况

然后将两种情况数相加就OK了
边界情况:
 当1出现在最高位的时候,第一种情况不存在
 当枚举0的时候,第一种情况就变成了1~abc-1因为数字不能有前导0

#include 
#include 
#include 

using namespace std;

const int N = 10;

/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/

int get(vector<int> num, int l, int r)//这个函数是用来求第一种情况的 
{
/*
就是求一下数,也就是样例中第一种情况的abc,或者之后 i==x的情况,求一下后边的数 
*/
    int res = 0;
    for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
    return res;
}

int power10(int x)//求一下10的i次方 
{
    int res = 1;
    while (x -- ) res *= 10;
    return res;
}

int count(int n, int x)
{
    if (!n) return 0;

    vector<int> num;
    while (n)
    {
        num.push_back(n % 10);
        n /= 10;
    }
    n = num.size();

    int res = 0;
    for (int i = n - 1 - !x; i >= 0; i -- )//从最高位开始枚举,因为用到了vector,所以从第n-1到0 
    {
        if (i < n - 1)//如果是枚举最高位,第一类情况不存在,只有枚举的不是最高位,这种情况才存在 
        {
            res += get(num, n - 1, i + 1) * power10(i);
            if (!x) res -= power10(i);//也就是前导不能为 0,从00...01开始枚举,也就是减去00...0 
        }

        if (num[i] == x) res += get(num, i - 1, 0) + 1;//+1是因为可以都为0 
        else if (num[i] > x) res += power10(i);// >x的时候,后边可从00..0 ~ 99...9 
    }

    return res;
}

int main()
{
    int a, b;
    while (cin >> a >> b , a)
    {
        if (a > b) swap(a, b);

        for (int i = 0; i <= 9; i ++ )
            cout << count(b, i) - count(a - 1, i) << ' ';
        cout << endl;
    }

    return 0;
}

状态压缩dp

例题
解析引用一遍不错的博客
状压dp:状态压缩动态规划,指把利用二进制表示的集合当作状态,在此基础上进行dp

状态压缩就一个思想,就是用一个整数来表示某一个状态,整数的话,先把他当成一个二进制数,然后二进制数里面的每一个位是0是1表示两种不同情况

状态压缩的题目有一个特点,由于我们要把所有的不同的状态压缩到一个整数里面,所以不同的状态个数应该不会很多,一般n==20就是极限了, 2 20 2^{20} 220就1e6了,所以很有特点,就看n比较小的时候能不能用状态压缩

也就是我们的状态虽然是一个整数,但是我们却需要把它看作一个二进制数,二进制数里面的每一位,是0是1表示不同的情况,一般不是很难,但是需要一些时间,来接受状态转移的方式和状态定义的方式
,思维难度比不上其他dp,理解套路就好

此题核心:先放横着的,再放竖着的,然后统计一下如果我们只放横着的小方块的话,合理的放法有多少种

当我们把横向小方格放完的时候,纵向一定只有一种方法,也就是,总方案数跟我们横向小方格摆放的方法一致
接着,开始求一下横向摆放小方格的方案数,这个可以按列来求,我们每一列用一个f[i,j]来表示
f[i,j]表示我现在要摆第i列,上一列中,横向摆放的小方格,压到第i列的个数为j个的情况下(如图这种,也就是,上一列中,哪些列伸出来了这样的一个小方格)的方案数,图中就是10000,

这里j是一个二进制的数,如图,图中j就是一个有五位(5行)的二进制数,可以表示0~31
dp第三弹~数位统计类dp,状压dp,树形dp_第2张图片
对题目情况举个例子
dp第三弹~数位统计类dp,状压dp,树形dp_第3张图片
第一个要注意的地方就是,不能冲突,从第i-1伸到第i列的 与 第i-2到i-1列的不能冲突,这个可以用位运算来判断,就是j&k是不是等于0,等于0就是没有冲突
第二个就是,所有剩余的连续格子一定得是偶数,也就是j|k不存在连续奇数个0,这个可以先预处理出来

只要满足这两个条件我们就可以把它们转移过来,就可以对应到我们的一种方案
然后遍历所有的k,看看是不是满足条件,加起来就行
在这里插入图片描述
时间复杂度:状态数是11* 2 11 2^{11} 211,转移状态的话,每一次需要计算 2 11 2^{11} 211,一共4e7左右

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include
using namespace std;
#define ll long long
#define fast ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define sc(a) scanf("%lld",&a)
#define pf(a) printf("%d",a) 
#define endl "\n"
#define int long long
#define mem(a,b) memset(a,b,sizeof a)
#define ull unsigned long long
#define INF 0x3f3f3f3f3f3f3f3f
#define inf 0x3f3f3f3f
#define rep(i,a,b) for(auto i=a;i<=b;++i)
#define bep(i,a,b) for(auto i=a;i>=b;--i)
#define lowbit(x) x&(-x)
#define PII pair<int,int>
#define PLL pair<ll,ll>
#define LL long long
#define PI acos(-1)
#define pb push_back
#define x first
#define y second
const double eps = 1e-6;
const int mod = 998244353;
const int MOD = 1e9 + 7;
const int N = 12, M = 1<<N;
int n, m;//n和m表示行数和列数 
int f[N][M];//状态转移的方程
bool st[M]; 
signed main()
{
	fast;
	while (cin >> n >> m, n || m)//读入n和m,并且不是两个0即合法输入就继续读入
    {
    	//第一部分:预处理1
        //对于每种状态,先预处理每列不能有奇数个连续的0
        for (int i = 0; i < 1 << n; i ++ )
        {
            int cnt = 0;//记录连续的0的个数
            st[i] = true;// 假设它是成立的,没有奇数个连续的0则标记为true
            for (int j = 0; j < n; j ++ )//看每一行 ,也就是给了每一个状态比如1100..1然后看看每一行啥情况 
                if (i >> j & 1) //i(i在此处是一种状态)的二进制数的第j位; &1为判断该位是否为1,是1表示一个连续的0段结束了 
                {
                    if (cnt & 1) st[i] = false;//如果连续段里面0的个数是奇数,就不合法 
                    cnt = 0;//进入下一段 
                }
                else cnt ++ ;
            if (cnt & 1) st[i] = false;
        }
		//dp部分
		 
        memset(f, 0, sizeof f);
        f[0][0] = 1;//最一开始的时候,小方块里面还什么都没有放,第0列的时候,从前边捅过来是0的方案是唯一一种 
        for (int i = 1; i <= m; i ++ )//枚举一下所有的列 
            for (int j = 0; j < 1 << n; j ++ )//枚举一下所有的状态 
                for (int k = 0; k < 1 << n; k ++ )//枚举第i-1列的所有状态 
                    if ((j & k) == 0 && st[j | k])//判断一下两个条件 
                        f[i][j] += f[i - 1][k];
		//枚举到第m列的时候,应该凸出来的是0,才是合法方案
		//我的理解是,它是数的从0到m-1行,算是它的答案行,然后最多就是捅到m-1当然不会到m 
        cout << f[m][0] << endl;
    }
	return 0;
} 

例题2哈密顿
如果暴力的话,我们就应该找一个序列,就是访问的顺序
可以全排列一下,看一下路径长度,这个时间复杂度很高,不可取

状压dp:跟上一个思路类似,用一个整数表示状态
集合:所有从0号点走到j号点,走过的所有点是i的路径,i是二进制数,每一位就表示该点走没走过
状态计算:这里根据倒数第二个点是哪个点来分类,假设倒数第二个点是k,状态方程就得到了
dp第三弹~数位统计类dp,状压dp,树形dp_第4张图片
然后对所有的k取一个min就行
dp第三弹~数位统计类dp,状压dp,树形dp_第5张图片

#include 
#include 
#include 

using namespace std;

const int N = 20, M = 1 << N;

int n;
int w[N][N];//路径权值 
int f[M][N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            cin >> w[i][j];

    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;//从0走到0,也就是00...1也就是1,终点是0,也就是f[1][0] 

    for (int i = 0; i < 1 << n; i ++ )
        for (int j = 0; j < n; j ++ )
            if (i >> j & 1)//走过的点里面必须要有j这个点才有意义 
                for (int k = 0; k < n; k ++ )
                    if (i >> k & 1)//表示走过这个点,而且倒数第二个点可以是这个点 
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);

    cout << f[(1 << n) - 1][n - 1];

    return 0;
}

树形dp

来个例题

集合f[u, 0]指的是所有以u为根的子树中选择,并且不选u这个点的方案, f[u, 1]指的是所有从以u为根的子树中选择,并且选u这个点的方案

dp第三弹~数位统计类dp,状压dp,树形dp_第6张图片

dp第三弹~数位统计类dp,状压dp,树形dp_第7张图片

求树形dp的时候,是从根节点开始递归求,递归到u这个点的时候,先把u的儿子处理好,也就是先算出来下面四个,算完之后再算f[u,0];这里不选u这个点,所以最大值就是两个子树的最大值,得到状态表达式
dp第三弹~数位统计类dp,状压dp,树形dp_第8张图片
时间复杂度:一共有2n个状态,每个状态枚举的时候需要枚举它每个儿子,所有节点儿子的数量加到一起就是边的数量,也就是n-1,所以计算所有的状态,我们枚举的次数一共是O(n),也就是时间复杂度O(n)
由于本题没有给根节点,所以需要自己求,没有父节点的点就是根节点

#include 
#include 
#include 

using namespace std;

const int N = 6010;

int n;
int h[N], e[N], ne[N], idx;
int happy[N];
int f[N][2];
bool has_fa[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u)
{
    f[u][1] = happy[u];

    for (int i = h[u]; ~i; i = ne[i])//~i等同于i!=-1 
    {
        int j = e[i];
        dfs(j);

        f[u][1] += f[j][0];
        f[u][0] += max(f[j][0], f[j][1]);
    }
}

int main()
{
    scanf("%d", &n);

    for (int i = 1; i <= n; i ++ ) scanf("%d", &happy[i]);

    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(b, a);//b是a的父节点 
        has_fa[a] = true;
    }

    int root = 1;
    while (has_fa[root]) root ++ ;//找父节点 

    dfs(root);

    printf("%d\n", max(f[root][0], f[root][1]));

    return 0;
}

你可能感兴趣的:(dp,动态规划,c++,算法)