分治

知识点

  1. 算法思想

    • :将原问题分解为k个规模较小的子问题(k≥2)
    • :递归求解各子问题
    • :合并子问题的解得到原问题解
  2. 适用条件

    • 问题可分解:原问题能划分为相同结构的子问题
    • 子问题独立:子问题间无重叠(区别于动态规划)
    • 解可合并:子问题解能有效合并为最终解

练习题

P1010 [NOIP 1998 普及组] 幂次方

题目描述

任何一个正整数都可以用 \(2\) 的幂次方表示。例如 $137=27+23+2^0 $。

同时约定次方用括号来表示,即 \(a^b\) 可表示为 \(a(b)\)

由此可知,\(137\) 可表示为 \(2(7)+2(3)+2(0)\)

进一步:

\(7= 2^2+2+2^0\) ( \(2^1\)\(2\) 表示),并且 \(3=2+2^0\)

所以最后 \(137\) 可表示为 \(2(2(2)+2+2(0))+2(2+2(0))+2(0)\)

又如 \(1315=2^{10} +2^8 +2^5 +2+1\)

所以 \(1315\) 最后可表示为 \(2(2(2+2(0))+2)+2(2(2+2(0)))+2(2(2)+2(0))+2+2(0)\)

输入格式

一行一个正整数 \(n\)

输出格式

符合约定的 \(n\)\(0, 2\) 表示(在表示中不能有空格)。

输入输出样例 #1

输入 #1

1315

输出 #1

2(2(2+2(0))+2)+2(2(2+2(0)))+2(2(2)+2(0))+2+2(0)

说明/提示

【数据范围】

对于 \(100\%\) 的数据,\(1 \le n \le 2 \times {10}^4\)

NOIP1998 普及组 第三题

解题思路

对最后生成的式子进行分析,发现只包含0和2两个数字,并且只由\(2(0)、2、2+2(0)、2(2)\)四个数字组合而成,分别对应十进制下的\(0、1、2、3\)

所以对输入数据进行二进制拆分、并且是从大到小开始拆,进行分治求解每一部分的表示方式,最后递归到一起获得最终式子。

对于给定的例子: 2的10次方可表示为2(10),再对括号中的10进行拆分,拆成\(2(3)+2\),再对括号中的\(3\)进行拆分,拆成\(2+2(0)\)

最后再合并起来就是\(2(2(2+2(0))+2)\)

参考代码

//p1010
#include <stdio.h>
#include <iostream>
#include <string>
using namespace std;
typedef pair<int, int> PII;
bool flag[25], flag1[25];

string print_set(int num){
    if(num == 1)    return "2(0)";
    else if(num == 2)   return "2";
    else if(num == 3)   return "2+2(0)";
    else if(num == 4)   return "2(2)";
    else{
        int value = 32768;
        int zs = 15;
        while(value > num){
            value /= 2;
            zs --;
        }
        if(num == value)    return "2(" + print_set(zs) + ")";
        else    return "2(" + print_set(zs) + ")+" + print_set(num - value);
    }
}

int main(){
    int n;
    cin >> n;
    cout << print_set(n) << endl;
    return 0;
}

P3612 [USACO17JAN] Secret Cow Code S

题目描述

奶牛们正在实验秘密代码,并设计了一种方法用于生成无限长度的字符串,作为他们代码的一部分。

给定一个字符串 \(s\),令 \(F(s)\)\(s\) 后接 \(s\) 向右“旋转”一个字符的结果(在右旋转中,\(s\) 的最后一个字符旋转并成为新的第一个字符)。给定初始字符串 \(s\),奶牛们通过重复应用 \(F\) 来构建他们的无限长度代码字符串;因此每一步都会使当前字符串的长度翻倍。

给定初始字符串和一个索引 \(N\),请帮助奶牛计算无限代码字符串中第 \(N\) 个位置的字符。

输入格式

输入由一行组成,包含一个字符串和 \(N\)。字符串最多由 30 个大写字母组成,且 \(N \leq 10^{18}\)

请注意,\(N\) 可能太大,无法放入标准的 32 位整数中,因此你可能需要使用 64 位整数类型(例如,C/C++ 中的 "long long")。

输出格式

请输出从初始字符串构建的无限代码字符串的第 \(N\) 个字符。第一个字符的位置为 \(N=1\)

输入输出样例 #1

输入 #1

COW 8

输出 #1

C

说明/提示

在这个例子中,初始字符串 COW 按以下方式扩展:

COW -> COWWCO -> COWWCOOCOWWC

解题思路

考虑暴力求解,\(N<=10^{18}\),会TLE

考虑逆向求解,求该无限字符串的第N个位置的字符。设其复制次数为\(t\),那么可以在\(O(t)\)的时间复杂度内求解得到在原字符串的位置,输出该位置的字符即可。

具体求解方法为:先通过倍增获得当前无限字符串拥有第\(N\)个位置时候的长度\(len\)。如果\(N\)\(len/2+1\),那么其代表的字符在上一个复制序列中处于最末尾的位置。否则,在原序列中位于\(N-len/2+1\)的位置。以此规则不断向前倒推,最后即可获得在原字符串中的位置。

参考代码

//p3612
#include <stdio.h>
#include <iostream>
#include <string.h>
using namespace std;
typedef long long ll;
ll n;
string s;

int main(){
    cin >> s >> n;
    ll len = s.length();
    ll val = len;
    while(val < n)    val *= 2;
    while(n > len){
        while(val >= n)  val /= 2;
        if(n == val + 1)    n = val;
        else    n = n - val - 1;
    }
    cout << s[n-1] << endl;
    return 0;
}

P1928 外星密码

题目描述

有了防护伞,并不能完全避免 2012 的灾难。地球防卫小队决定去求助外星种族的帮助。经过很长时间的努力,小队终于收到了外星生命的回信。但是外星人发过来的却是一串密码。只有解开密码,才能知道外星人给的准确回复。解开密码的第一道工序就是解压缩密码,外星人对于连续的若干个相同的子串 \(\texttt{X}\) 会压缩为 \(\texttt{[DX]}\) 的形式(\(D\) 是一个整数且 \(1\leq D\leq99\)),比如说字符串 \(\texttt{CBCBCBCB}\) 就压缩为 \(\texttt{[4CB]}\) 或者\(\texttt{[2[2CB]]}\),类似于后面这种压缩之后再压缩的称为二重压缩。如果是 \(\texttt{[2[2[2CB]]]}\) 则是三重的。现在我们给你外星人发送的密码,请你对其进行解压缩。

输入格式

输入一行,一个字符串,表示外星人发送的密码。

输出格式

输出一行,一个字符串,表示解压缩后的结果。

输入输出样例 #1

输入 #1

AC[3FUN]

输出 #1

ACFUNFUNFUN

说明/提示

【数据范围】

对于 \(50\%\) 的数据:解压后的字符串长度在 \(1000\) 以内,最多只有三重压缩。

对于 \(100\%\) 的数据:解压后的字符串长度在 \(20000\) 以内,最多只有十重压缩。保证只包含数字、大写字母、[]

解题思路

一道数据有一、、恶心的题目TAT,而且感觉代码写得不是很优雅

考虑题目逻辑。一旦遇到了符号\([\) ,证明存在压缩字符串,需要对字符串进行解压缩处理。

解压缩函数\(pr()\)的逻辑为:进入该函数,证明遇到了符号 '[' ,并且该符号后跟着数字,数字后跟着大写字母,大写字母后跟着符号']'。

但是符号'[]'内可能存在多重压缩如AC[3[3FUN]],因此可能存在迭代调用\(pr()\)函数的情况。

还有可能存在压缩字符串中存在压缩和非压缩字符串如[3A[2B]C]。

解决方法为加一个外层while循环,判断对于当前的' [ '一定要找到一个与之匹配的' ] ',找到需要重复的字符串,乘上' [ '后的次数,才代表对于当前解压缩任务,完整地返回了原本的字符串。

参考代码

//p1928
#include <stdio.h>
#include <iostream>
#include <string.h>
using namespace std;
string s;
int idx;

string pr(){
    //处理当前字符串
    string temp = "";
    string temp1 = "";
    //处理乘的次数
    int now = 0;
    while(s[idx] != ']'){
        while('0' <= s[idx] && s[idx] <= '9'){
            now *= 10;
            now += (s[idx] - '0');
            idx ++;
        }
        while('A' <= s[idx] && s[idx] <= 'Z'){
            temp += s[idx];
            idx ++;
        }
        if(s[idx] == '['){
            idx ++;
            temp += pr();
        }
    }
    if(now == 0)    temp1 += temp;
    for(int i = 1; i <= now; i++)     temp1 += temp;
    idx ++;
    return temp1;
}

int main(){
    cin >> s;
    int len = int(s.length());
    while(idx < len){
        if(s[idx] == '['){
            idx ++;
            cout << pr();
        }
        else if(s[idx] == ']')  idx ++;
        else{
            cout << s[idx];
            idx ++;
        }
    }
    cout << endl;
    return 0;
}

P1498 南蛮图腾

题目描述

自从到了南蛮之地,孔明不仅把孟获收拾的服服帖帖,而且还发现了不少少数民族的智慧,他发现少数民族的图腾往往有着一种分形的效果,在得到了酋长的传授后,孔明掌握了不少绘图技术,但唯独不会画他们的图腾,于是他找上了你的爷爷的爷爷的爷爷的爷爷……帮忙,作为一个好孙子的孙子的孙子的孙子……你能做到吗?

给定一个正整数 \(n\),参考输出样例,输出图形。

输入格式

每个数据输入一个正整数 \(n\),表示图腾的大小(此大小非彼大小)

输出格式

这个大小的图腾

输入输出样例 #1

输入 #1

2

输出 #1

   /\
  /__\
 /\  /\
/__\/__\

输入输出样例 #2

输入 #2

3

输出 #2

       /\
      /__\
     /\  /\
    /__\/__\
   /\      /\
  /__\    /__\
 /\  /\  /\  /\
/__\/__\/__\/__\

说明/提示

数据保证,\(1 \leq n \leq 10\)

解题思路

输出三角形时,最重要的问题在于确定其左下角的位置。

因此在函数中的参数设置:

1)当前三角形左下角坐标\((x,y)\)

2)确定当前三角形的长和宽\(w、h\)以及当前还需要迭代的次数\(cs\)

3)确定迭代的三个小三角形的位置,分别为\((x,y)、(x, y+w/2)、(x-h/2,y+w/4)\)

4)当\(cs==1\)时,即可以确定最小单位三角形的左下角坐标,向字符数组写入,最后遍历数组输出。

参考代码

//p1498
#include <stdio.h>
#include <iostream>
using namespace std;
char tri[1000][1000];

//输入确定三角形的左下角点的坐标
void gz(int x, int y, int cs, int w, int h){
    if(cs == 1){
        tri[x][y] = '/';
        tri[x][y+1] = '_';
        tri[x][y+2] = '_';
        tri[x][y+3] = '\\';
        tri[x-1][y+1] = '/';
        tri[x-1][y+2] = '\\';
        return;
    }
    gz(x, y, cs - 1, w / 2, h / 2);
    gz(x, y  + w / 2, cs - 1, w / 2, h / 2);
    gz(x - h / 2, y + w / 4, cs - 1, w / 2, h / 2);
}

int main(){
    int n;
    cin >> n;
    int x_pos = 2, y_pos = 4;
    for(int i = 1; i < n; i++){
        x_pos *= 2;
        y_pos *= 2;
    }
    gz(x_pos, 1 , n, y_pos, x_pos);
    for(int i = 1; i <= x_pos; i++){
        for(int j = 1; j <= y_pos; j++){
            if(!tri[i][j])     cout << " ";
            else    cout << tri[i][j];
        }
        cout << endl;
    }
    return 0;
}

P4141 消失之物

题目描述

ftiasch 有 \(n\) 个物品, 体积分别是 \(w_1,w_2,\dots,w_n\)。由于她的疏忽,第 \(i\) 个物品丢失了。

“要使用剩下的 \(n-1\) 物品装满容积为 \(x\) 的背包,有几种方法呢?”——这是经典的问题了。

她把答案记为 \(\text{cnt}(i,x)\) ,想要得到所有\(i \in [1,n]\), \(x \in [1,m]\)\(\text{cnt}(i,x)\) 表格。

输入格式

第一行两个整数 \(n,m\),表示物品的数量和最大的容积。
第二行 \(n\) 个整数 \(w_1,w_2,\dots,w_n\),表示每个物品的体积。

输出格式

输出一个 \(n \times m\) 的矩阵,表示 \(\text{cnt}(i,x)\)末位数字

输入输出样例 #1

输入 #1

3 2
1 1 2

输出 #1

11
11
21

说明/提示

【数据范围】
对于 \(100\%\) 的数据,\(1\le n,m \le 2000\),且 \(1\le v_i\le m\)

【样例解释】
如果物品 3 丢失的话,只有一种方法装满容量是 2 的背包,即选择物品 1 和物品 2。

解题思路

题目第一眼看上去是背包,特殊的地方在于输出的矩阵\(cnt[i][j]\)表示当第\(i\)个物品消失,只剩下其余的\(n-1\)个物品时,凑成容积为\(j\)的情况方案数(末位数字,注意对10的取模运算)。

考虑暴力解法,如果把第\(i\)个物品排除之后生成一个新序列,对该序列进行01背包问题求解,那么总时间复杂度为\(O(n^2m)\),因为\(n,m<=2000\),总时间复杂度会达到\(O(10^{10})\)数量级。因此考虑对第一个\(n\)进行优化。

简化题目所给条件,先不考虑第\(i\)个物品不选,考虑所有物品都能选的情况下,用数组\(f[i]\)表示总体积为\(i\)时的方案数量,为正常01背包,时间复杂度为\(O(n*m)\)

考虑不选第\(i\)个物品的情况:

  • \(j<w[i]\)时,第\(i\)个物品一定不会被选,此时\(cnt[i][j] = f[j]\)
  • \(j>=w[i]\)时,第\(i\)个物品有可能被选上,总方案数需要扣掉选上第\(i\)个物品的方案数。逆向思维来想,选上了第\(i\)个物品,并且总体积为\(j\)的方案数为\(cnt[i][j-w[i]]\),也就是不选\(i\)且总体积为\(j-w[i]\)的方案数等价于选了\(i\)且总体积为\(j\)的方案数;
  • 因此可以得出,\(cnt[i][j]=f[j]-cnt[i][j-w[i]]\),其时间复杂度为\(O(nm)\)。并且cnt数组只有对该行有效,可以压缩成一维。

参考代码

//p4141
#include <stdio.h>
#include <iostream>
using namespace std;
typedef long long ll;
ll w[2005], f[2005], cnt[2005];

int main(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)     cin >> w[i];
    f[0] = 1;
    for(int i = 1; i <= n; i++){
        for(int j = m; j >= 0; j--){
            if(j >= w[i])   f[j] = (f[j] + f[j-w[i]]) % 10;
        }
    }
    cnt[0] = 1;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(j < w[i])    cnt[j] = f[j];
            else    cnt[j] = (f[j] - cnt[j - w[i]] + 10) % 10;
            cout << cnt[j] % 10;
        }
        cout << endl;
    }
    return 0;
}
posted @ 2025-07-06 10:23  hsy2093  阅读(21)  评论(0)    收藏  举报