当前位置:首页 > 技术 > 正文内容

数字计数、状态压缩与树形动态规划技术解析

访客 技术 2026年6月5日 1

数字计数问题

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

int digitCount(int num) // 计算整数num有多少位
{
    int result = 0;
    while (num) ++ result, num /= 10;
    return result;
}

int countDigit(int upper, int digit) // 计算从1到upper的整数中数字digit出现多少次 
{
    int result = 0, digits = digitCount(upper);
    for (int position = 1; position <= digits; ++ position) // 从右到左第position位上 数字digit出现多少次 
    {
        // left和right是第position位左边和右边的整数; digitValue是第position位的数字
        int power = pow(10, position - 1), left = upper / power / 10, right = upper % power, digitValue = upper / power % 10;
        // 计算第position位左边的整数小于left的情况 左边不等于left的时候 说明都是比left小的数字  
        if (digit) result += left * power; //如果不是统计数字0 左边直接乘power就行了 upper=ab3xxx power=1000  
//upper=1236055 6000-6999这里1000  第position位上的6出现了power次 但是左边还有16000-16999 26000-26999 36000-36999...1226000-12269999 共左边数字left(即123)个 所以是left*power 
        else if (!digit && left) result += (left - 1) * power; // 统计的数字digit = 0, 左边高位不能全为0 
//少了0000-0999的一种情况 从10000-10999 开始 ... 1220000-1220999 13000-13999 共(left-1)次 

// 计算第position位左边的整数等于left的情况 只会和*position位后面的数*有关



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

        if ( (digitValue == digit) && (digit || left) ) result += right + 1;//这是right有多少个 就是多少个+1
//if(digitValue==digit) upper=1236055  1236000-1236055   即55+1种情况
//当统计的数字digit==0 且 left==0, 举例  upper=123456 left==0且digit==0 就是000000 -0123456 而这个时候显然和 第position-1的位的时候重复了100000-109999

//if(digitValue>digit) upper=1236000 则有1237000-1237999 所以是0 

    }
    return result;
}

int main()
{
    int start, end;
    while (cin >> start >> end , start)
    {
        if (start > end) swap(start, end);
        for (int digit = 0; digit <= 9; ++ digit) cout << countDigit(end, digit) - countDigit(start - 1, digit) << ' ';
        cout << endl;
    }
    return 0;
}

地板铺装问题

由于要求完全铺满,只需确定横向铺设方案,纵向铺设方案也随之确定。因此,我们只需计算横向铺设的方案数量。 逐列分析,设dp[i][j]表示前i-1列已铺设完成,j表示当前列的状态,j的每一位为1表示该行向右延伸。 两个转移条件:i列和i-1列的同一行不能同时延伸;当前列状态j与前一列状态k进行或运算,得到是否有奇数空行状态,奇数空行状态不允许转移。j&k==0 初始化条件dp[0][0] = 1,第0列只能是状态0,无任何格子延伸。最终返回dp[m][0]。第m+1列不能有任何延伸。 st数组预处理每个状态,标记该状态中连续空格是否均为偶数个,若是则为true,否则为false 判断二进制数i的第j位是否为1,为1表示第j行放置木块,不为1则不放置。

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

const int ROWS=12, COLS = 1<< ROWS;  

long long dp[ROWS][COLS] ;// 第一维表示列数, 第二维表示所有可能的状态

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

vector<vector<int>> transitions(COLS);  //记录每个状态可以转移的前置状态
int rows , columns;

int main(){

    while(cin>>rows>>columns, rows||columns){ //读入行数和列数,并且不是两个0即合法输入就继续读入

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

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

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

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

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

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

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

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

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

                    transitions[current].push_back(prev);  //二维数组transitions[current]表示当前列为current状态时,可以从前一列的prev状态转移而来
            }

        }

        //第三部分:dp开始

        memset(dp,0,sizeof dp);  //全部初始化为0,因为是连续读入,这里是一个清空操作。

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

        for(int col=1;col<= columns;col++){ //遍历每一列:第col列合法范围是(0~columns-1列)
            for(int current_state=0; current_state< 1<<rows; current_state++){  //遍历当前列(第col列)每一行的总和 所有状态current_state 
                for( auto prev_state : transitions[current_state])    // 遍历前一列 col-1列的状态prev_state,如果转移关系成立,就转移 而前一列的状态dp[col-1][prev_state]又有自带的属性

                    dp[col][current_state] += dp[col-1][prev_state];    // 当前列的方案数就等于之前的第col-1列所有状态prev_state的累加。
            }
        }

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

        cout<< dp[columns][0]<<endl;





    }
}   

简化版
#include<bits/stdc++.h>
using namespace std;
const int ROWS = 12, COLS = 1 << ROWS;
int valid[COLS];
long long dp[ROWS][COLS];


int main(){
    int rows, columns;
    while (cin >> rows >> columns && (rows || columns)){

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

        memset(dp, 0, sizeof dp);
        dp[0][0] = 1;
        for (int col = 1; col <= columns; col ++)
            for (int current = 0; current < 1 << rows; current ++)
                for (int prev = 0; prev < 1 << rows; prev ++)
                    if ((current & prev) == 0 && (valid[current | prev])) 
                    // current & prev == 0 表示 col 列和 col - 1列同一行不同时延伸
                    // valid[current | prev] == 1 表示 在 col 列状态 current, col - 1 列状态 prev 的情况下是合法的.
                        dp[col][current] += dp[col - 1][prev];      
        cout << dp[columns][0] << endl;
    }
    return 0;
}

最短哈密顿路径

注意:加减运算符的优先级比位运算高!

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

using namespace std;

const int NODES = 20, STATES = 1 << NODES;

int nodeCount;
int distanceMatrix[NODES][NODES];
int dp[STATES][NODES];//STATES存储的最大状态 
//nodeCount表示当前到达了第j个点

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

    memset(dp, 0x3f, sizeof dp);//初始为正无穷
    dp[1][0] = 0;//j表示到达了第0个点 i表示经过的是00001也就是第1个点 而这个值是0 不是正无穷

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

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

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。