加载中...

数位dp 状压dp 树形dp(围绕某个点为根节点)

计数问题 https://www.acwing.com/problem/content/340/

# include <iostream>
# include <cmath>
using namespace std;

int dgt(int n) // 计算整数n有多少位
{
    int res = 0;
    while (n) ++ res, n /= 10;
    return res;
}

int cnt(int n, int i) // 计算从1到n的整数中数字i出现多少次 
{
    int res = 0, d = dgt(n);
    for (int j = 1; j <= d; ++ j) // 从右到左第j位上 数字i出现多少次 
    {
        // l和r是第j位左边和右边的整数 (视频中的abc和efg); dj是第j位的数字
        int p = pow(10, j - 1), l = n / p / 10, r = n % p, dj = n / p % 10;
        // 计算第j位左边的整数小于l (视频中l = 000 ~ abc - 1)的情况 左边不等于abc的时候 说明都是比abc小的数字  
        if (i) res += l * p; //如果不是统计数字0 左边直接乘p就行了 n=ab3xxx p=1000  
//n=1236055 6000-6999这里1000  第j位上的6出现了p次 但是左边还有16000-16999 26000-26999 36000-36999...1226000-1226999 共左边数字l(即123)个 所以是l*p 
        else if (!i && l) res += (l - 1) * p; // 统计的数字i = 0, 左边高位不能全为0(视频中xxx = 001 ~ abc - 1) 
//少了0000-0999的一种情况 从10000-10999 开始 ... 1220000-1220999 13000-13999 共(l-1)次 

// 计算第j位左边的整数等于l (视频中l = abc)的情况 只会和*j位后面的数*有关



//下面就是l的左边相等的情况 对第j位上 不会多算6000-6999 ...1226000-1226999里面的任意个集合 123开始的情况
        if ( (dj > i) && (i || l) ) res += p;//第j位比现在统计的数字大 就可以直接加上p中情况 
// n=1236055  则有1235000-1235999 999+1种情况 即p种 
//当统计的数字i==0 且 l==0, 举例  n=123456 l==0 第j位为1  就是p=100000 此时000000-099999是不成立的 因为我要统计第j位为i的时候 有多少个这样的 数 而此时   000000-099999 显然和 100000-199999 第j-1位为2的时候重复了

        if ( (dj == i) && (i || l) ) res += r + 1;//这是r有多少个 就是多少个+1
//if(dj==i) n=1236055  1236000-1236055   即55+1种情况
//当统计的数字i==0 且 l==0, 举例  n=123456 l==0且i==0 就是000000 -0123456 而这个时候显然和 第j-1的位的时候重复了100000-109999

//if(dj>i) n=1236000 则有1237000-1237999 所以是0 

    }
    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 << cnt(b, i) - cnt(a - 1, i) << ' ';
        cout << endl;
    }
    return 0;
}



铺地砖 竖着铺地砖横着铺地砖 要求恰好铺满

因为需要恰好铺满所以只要横着铺地砖的方案数确定了 那么竖着铺的也确定了 所以只要求横着铺放有多少种就好了
一列列看 加入f[i][j]表示 第i-1列已经铺好了 j表示这一列的状态 j上每一位的1表示这一行向右伸出
4. 两个转移条件: i 列和 i - 1列同一行不同时捅出来 ; 本列捅出来的状态j和上列捅出来的状态k求或,得到上列是否为奇数空行状态,奇数空行不转移。 j&k==0
5. 初始化条件f[0][0] = 1,第0列只能是状态0,无任何格子捅出来。返回f[m][0]。第m + 1列不能有东西捅出来。
6.我理解的是st数组预处理的是针对每一个状态,标记这一状态连续空着的格子是不是都是偶数个,如果是就为true,不是就为false
7.判断二进制数字i的第j位是否为1 为1则对应于第j行放木块的情况 不为1则不放。我觉得应该是这样

#include<bits/stdc++.h>
using namespace std;

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

long long f[N][M] ;// 第一维表示列, 第二维表示所有可能的状态

bool st[M];  //存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。

//vector<int > state[M];  //二维数组记录合法的状态
vector<vector<int>> state(M);  //两种写法等价:二维数组
int m , n;

int main(){

    while(cin>>n>>m, n||m){ //读入n和m,并且不是两个0即合法输入就继续读入

        //第一部分:预处理每个st状态
        //对于每种状态,先预处理每列不能有奇数个连续的0

        for(int i=0; i< 1<<n; i++){//共有1<<n个数 数二进制表示n个行的状态

            int cnt =0 ;//记录连续的0的个数

            bool isValid = true; // 某种状态没有奇数个连续的0则标记为true

            for(int j=0;j<n;j++){ //从上到下遍历每一行

                 if( i>>j &1){  //i>>j位运算,表示i(i在此处是一种状态)的二进制数的第j位; &1为判断该位是否为1,如果为1进入if
                    if(cnt &1) { //这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
                        isValid =false;break;
                    } 
                    cnt=0; // 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。//其实清不清零没有影响
                 }
                 else cnt++; //否则的话该位还是0,则统计连续0的计数器++。
            }
            if(cnt &1)  isValid =false; //最下面的那一段判断一下连续的0的个数

            st[i]  = isValid; //状态i是否有奇数个连续的0的情况,输入到数组st中
        }

        //第二部分:预处理2
        // 经过上面每种状态 连续0的判断,已经筛掉一些状态。
        //下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突

        for(int j=0;j< 1<<n;j++){ //对于每一个状态
            state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
            for(int k=0;k< 1<<n;k++){ //对于第i-1列所有状态
                if((j&k )==0 && st[ j| k] ) // 第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行 如果相同位置上有1就 j和k会不会是0了) 且这个状态有偶数个0
                //解释一下st[j | k] 
                //已经知道st[]数组表示的是这一列没有连续奇数个0的情况,
                //我们要考虑的是第i-1列(第i-1列是这里的主体)中从第i-2列横插过来的,还要考虑自己这一列(i-1列)横插到第i列的
                //比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
                //那么合在第i-1列,到底有多少个1呢?自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
                //这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的

                    state[j].push_back(k);  //二维数组state[j]表示第j行, state[j]里存放的是对应前面一列的可行的状态
                    //j表示 第i列“真正”可行的状态,如果第i-1列的状态k和j不冲突则压入state数组中的第j行。
                    //“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
            }

        }

        //第三部分:dp开始

        memset(f,0,sizeof f);  //全部初始化为0,因为是连续读入,这里是一个清空操作。类似上面的state[j].clear()

        f[0][0]=1 ;// 这里需要回忆状态表示的定义,按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
        //首先,这里没有-1列,最少也是0列。其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。

        for(int i=1;i<= m;i++){ //遍历每一列:第i列合法范围是(0~m-1列)
            for(int j=0; j< 1<<n; j++){  //遍历当前列(第i列)每一行的总和 所有状态j 
                for( auto k : state[j])    // 遍历第前一列 i-1列的状态k,如果“真正”可行,就转移 而前一列的状态f[i-1][k]又有自带的属性

                    f[i][j] += f[i-1][k];    // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
            }
        }

        //最后答案是什么呢?
        //f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
        //即整个棋盘处理完的方案数

        cout<< f[m][0]<<endl;





    }
}   

作者:shizhengLee
链接:https://www.acwing.com/solution/content/28088/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

简易
#include<bits/stdc++.h>
using namespace std;
const int N = 12, M = 1 << N;
int st[M];
long long f[N][M];


int main(){
    int n, m;
    while (cin >> n >> m && (n || m)){

        for (int i = 0; i < 1 << n; i ++){
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++)
                if (i >> j & 1){
                    if (cnt & 1) st[i] = false; // cnt 为当前已经存在多少个连续的0
                    cnt = 0;
                }
                else cnt ++;
            if (cnt & 1) st[i] = false; // 扫完后要判断一下最后一段有多少个连续的0
        }

        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++)
            for (int j = 0; j < 1 << n; j ++)
                for (int k = 0; k < 1 << n; k ++)
                    if ((j & k) == 0 && (st[j | k])) 
                    // j & k == 0 表示 i 列和 i - 1列同一行不同时捅出来
                    // st[j | k] == 1 表示 在 i 列状态 j, i - 1 列状态 k 的情况下是合法的.
                        f[i][j] += f[i - 1][k];      
        cout << f[m][0] << endl;
    }
    return 0;
}

、

最短Hamilton路径https://www.acwing.com/problem/content/93/

+-法的优先级比位运算高!!!

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

using namespace std;

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

int n;
int w[N][N];
int f[M][N];//M存储的最大状态 
//n表示当前到达了第j个点

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;//j表示到达了第0个点 i表示经过的是00001也就是第1个点 而这个值是0 不是正无穷

    for (int i = 0; i < 1 << n; i ++ )//对每个n个状态的数
        for (int j = 0; j < n; j ++ )//对所有的点 设置为终点 
            if (i >> j & 1)//需要包含j个点 
才需要进行状态转移 否则没有意义 
                for (int k = 0; k < n; k ++ )//枚举所有k尝试找到一个中间点
                    if (i >> k & 1)//但需要这个状态包含k这个点
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
//i-(1<<j) 表示出去j这个点 到达k +k 到达j的值的最小值

    cout << f[(1 << n) - 1][n - 1];
//第一个表示 每一个n都是1 只需要二进制下1000000000-1就变成少了一位的111111111了 表示n个点都走完了 落脚到n-1这个点(从0开始)的最小值
    return 0;
}

posted @ 2022-02-26 12:54  liang302  阅读(57)  评论(0编辑  收藏  举报