惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
The GitHub Blog
The GitHub Blog
C
Check Point Blog
博客园_首页
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
F
Full Disclosure
Microsoft Security Blog
Microsoft Security Blog
爱范儿
爱范儿
Recent Announcements
Recent Announcements
阮一峰的网络日志
阮一峰的网络日志
G
GRAHAM CLULEY
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Threat Research - Cisco Blogs
C
Cybersecurity and Infrastructure Security Agency CISA
V
Vulnerabilities – Threatpost
K
Kaspersky official blog
博客园 - 司徒正美
S
Schneier on Security
T
The Exploit Database - CXSecurity.com
Project Zero
Project Zero
云风的 BLOG
云风的 BLOG
Cisco Talos Blog
Cisco Talos Blog
Know Your Adversary
Know Your Adversary
雷峰网
雷峰网
V
V2EX - 技术
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Spread Privacy
Spread Privacy
罗磊的独立博客
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
Security Affairs
SecWiki News
SecWiki News
Schneier on Security
Schneier on Security
O
OpenAI News
Jina AI
Jina AI
PCI Perspectives
PCI Perspectives
Cyberwarzone
Cyberwarzone
Y
Y Combinator Blog
Apple Machine Learning Research
Apple Machine Learning Research
B
Blog RSS Feed
I
InfoQ
D
Docker
P
Palo Alto Networks Blog
Recorded Future
Recorded Future
M
MIT News - Artificial intelligence
博客园 - Franky
B
Blog
Scott Helme
Scott Helme
博客园 - 叶小钗
D
DataBreaches.Net

博客园 - hoodlum1980

【Release】Photoshop ICO file format plug-in v3.0 (supports both x86 and x64) 【发布】Photoshop ICO 文件格式插件 3.0 (支持 x64) [发布] 一个测试 WebService 和数据库连接的高性能工具 - DBTest ZOJ 1095. Humble Numbers 去除搜狗输入法弹窗骚扰的一个简易方法 ZOL 3977. Pointers 高斯模糊算法的 C++ 实现 对象布局已知时 C++ 对象指针的转换时地址调整 采用栈数据结构的二叉树非递归遍历 ZOJ 3481. Expand Tab - hoodlum1980 “金山杯2007逆向分析挑战赛”第一阶段第二题 - hoodlum1980 “金山杯2007逆向分析挑战赛”第一阶段第一题分析 - hoodlum1980 对《神奇的C语言》文中例子 5 代码的分析讨论 对"QQGame-大家来找茬"的辅助工具的改进 memset 的实现分析 ZOJ 1958. Friends - hoodlum1980 Dell笔记本刷回低版本bios的方法 [发布] Photoshop 绘制表格滤镜(DrawTable) 采用路径模型实现遍历二叉树的方法
ZOJ 1004. Anagrams by Stack 解题报告
hoodlum1980 · 2025-10-14 · via 博客园 - hoodlum1980

题目链接:《1004: Anagrams by Stack》

为了防止题目链接失效,将题目原文复制如下:

1004. Anagrams by Stack How can anagrams result from sequences of stack operations? There are two sequences of stack operators which can convert TROT to TORT:

[
i i i i o o o o
i o i i o o i o
]

where i stands for Push and o stands for Pop. Your program should, given pairs of words produce sequences of stack operations which convert the first word to the second.

Input Specification:

The input will consist of several lines of input. The first line of each pair of input lines is to be considered as a source word (which does not include the end-of-line character). The second line (again, not including the end-of-line character) of each pair is a target word. The end of input is marked by end of file.

Output Specification:

For each input pair, your program should produce a sorted list of valid sequences of i and o which produce the target word from the source word. Each list should be delimited by

[
]

and the sequences should be printed in "dictionary order". Within each sequence, each i and o is followed by a single space and each sequence is terminated by a new line.

Process

A stack is a data storage and retrieval structure permitting two operations:

  • Push - to insert an item and
  • Pop - to retrieve the most recently pushed item

We will use the symbol i (in) for push and o (out) for pop operations for an initially empty stack of characters. Given an input word, some sequences of push and pop operations are valid in that every character of the word is both pushed and popped, and furthermore, no attempt is ever made to pop the empty stack. For example, if the word FOO is input, then the sequence:
SequenceExplanation
i i o i o o is valid, but
i i o is not (it's too short), neither is
i i o o o i (there's an illegal pop of an empty stack)

Valid sequences yield rearrangements of the letters in an input word. For example, the input word FOO and the sequence i i o i o o produce the anagram OOF. So also would the sequence i i i o o o. You are to write a program to input pairs of words and output all the valid sequences of i and o which will produce the second member of each pair from the first.

Sample Input:

madam
adamm
bahama
bahama
long
short
eric
rice

Sample Output:

[
i i i i o o o i o o
i i i i o o o o i o
i i o i o i o i o o
i i o i o i o o i o
]
[
i o i i i o o i i o o o
i o i i i o o o i o i o
i o i o i o i i i o o o
i o i o i o i o i o i o
]
[
]
[
i i o i o i o o
]

  • 题意:

通过栈完成回文字符串:给定两个单词 word1 和 word2,假设有一个字符栈(stack),通过对字符的栈操作(i 为 push 入栈操作,o 为 pop 出栈操作),可能把 word1 转变为 word2,那么这一系列的栈操作(由字母 i 和 o 组成)就是一个可行的操作。题目要求输出所有可以完成把 w1 转变为 w2 的操作,并按照字典顺序输出它们。

例如把 "TROT" 转变为 "TORT",则以下两种操作序列,都可以做到:

[
    i i i i o o o o
    i o i i o o i o
]

  • 分析:

这是一个困扰了我很多年的问题,从我最开始做 ZOJ 题目开始,就遇到这个问题,虽然这个题目看起来好像非常的简单,但我常年来一直没想到一个比较好的解题办法,所以这道题一直搁置,但是它一直在我的心理。表面上看这道题有子问题,很类似动态规划类题目,但是因为它每个子问题都有多组解,所以常规的解决动态规划问题使用的二维矩阵那种存储解的方法又并不合适。用穷举法,显然也不合适。所以我想来想去还是没想好这个题目的做法。直到昨天,我决定去做一下这道题。

题意里讲到的栈操作本身是非常直观的,大家都知道栈具有 FILO (先进后出)的特点,所以我们把一个字符 push 到栈中以后,可以稍后再 pop 出,这就实现了把一个字符“稍晚”再输出的效果,也就是相当于能把一个字符和它后面的多个字符,颠倒它们之间的顺序(假设我们不 care 后面的多个字符之间的排列)。比如说,假如有一个字符 a 位于单词 word1 的第一个字符,转换后我们让 a 位于单词 word2 的最后一个字符:

  word1: a[xxxx...]

  word2: [xxxx...]a

对于这样两个单词,可以用 "i [......] o" 这样的操作([......] 是对后面多个字符的合法栈操作序列,并能将这些字符全部输出)达到让 a 出现在它后面 n 个字符的后面的输出结果。当然,[xxxx...] 这多个字符之间可能会顺序比较混乱,目前我们不在意这一点。这样,我们就可以令 a 和它后面的多个字符颠倒顺序,让 a 出现在输出单词的最后面。

不难得出结论有且仅有 "i [ ... ] o" 这一种操作,可以让长度为 len 的 word1 的首个字符出现在 word2 的结尾位置。其中:[ ... ] 是对 (len - 1) 个字符的任一合法栈操作序列。

对 n 个字符的合法栈操作序列,是指对连续的 n 个字符中的每个字符都入栈和出栈过一次,且所有操作合法,操作后 stack 栈顶指针的指向位置和执行该操作序列之前相同。也就是说,该操作序列会把 n 个字符进行重新排列并产生一个新的 n 个字符的输出结果。

这一点很重要,有了这个基本结论,就可以开始把原问题拆解为规模更小的子问题。

我们在 word2 中逐个字符检查,看它是不是 word1 的首个字符,如果在 word2 的某个位置的字符,是 word1 的首个字符,那么问题将会分解为如下:

ZOJ1004_02

因此解决该问题的方法是:从 word2 的第一个字符开始,遍历 word2 的每个字符,如果 word2 的 i-th (0-based) 字符和 word1 的首字符(假设为 a)相同,那么在索引为 i 的位置,就可以把原问题分解为两个子问题:

  1. word1 中位于 a 后面的 i 个字符,如何通过合法栈操作,转变为 word2 中的前 i 个字符。
  2. word1 中尾部的 (len - i - 1) 个字符,如何通过合法栈操作,转变为 word2 中的尾部的 (len - i - 1) 个字符。

如果有了以上的子问题的解,就可以得出当前问题的一组解:把  a 入栈,加上子问题(1)的解,把 a 出栈,加上子问题(2)的解,就是当前问题的解。

如果把子问题(1)的解集 (注意:包含多个解),称为 child1,把子问题(2)的解集 (注意:包含多个解),称为 child2,那么当前问题的解将会增加 child1.size() * child2.size() 个。可以把新增加的这些解,表示为:

{i + Child1 + o} × {Child2}

当然,前提是子问题(1)和(2)必须都有解。可以看出,子问题(1)和(2)和当前问题属于本质相同的问题,这启发我们可以使用递归函数来解决这个问题。

首先,定义解决问题的递归函数的原型如下:

typedef struct tagSTR
{
char x[256]; } STR, *PSTR; //把 word1 通过栈操作转换为 word2,所有可能的操作序列,被存储在 results 中。 //[in] const char* word1: 第一个单词。 //[in] const char* word2: 第二个单词,长度和 word1 相同。 //[in] int len: word1 和 word2 包含的字符数量 (strlen)。 //[out] std::vector<STR> &results: 存储从 word1 转换到 word2 的所有栈操作序列。 //返回值: bool 是否有解。返回值 = !results.empty(); bool Convert(const char* word1, const char* word2, int len, std::vector<STR> &results);

然后,定义规模最小问题的解(回归条件),为了求解代码的一致性和简洁性,对问题边界做以下定义:

  1. 当 len = 0 时,有一个解:长度为 0 的空字符串(注:非 NULL 值)。
  2. 当 len = 1 时,如果两个单词的首字符相同,则有一个解:io,否则问题无解。

在函数返回时,通过判断 results.empty() 的值也可以判断问题是否有解,results 为空集合则无解。

如果任何一个子问题无解,则在当前索引 i 的位置无解,继续检查 word2 的下一个字符。当找到所有解后,再对包含所有解的容器做一次按升序排序即可。

  • 用于提交的代码:

#include <vector>
#include <algorithm>

typedef struct tagSTR
{
    char x[256];
} STR, *PSTR;

//判断两个元素是否已经是“有序”排列。(从小到大排序)
bool ASC(STR s1, STR s2);


//把 word1 通过 stack 操作转换为 word2,操作序列存储在 results 中。
//
//[in]  const char* word1: 第一个单词。
//[in]  const char* word2: 第二个单词。
//[in]  int len: word1 和 word2 的长度 (strlen)。
//[out] std::vector<STR> &results: 存储从 word1 转换到 word2 的所有操作。
//[return] bool: 是否有解。

bool Convert(const char* word1, const char* word2, int len, std::vector<STR> &results);


int main(int argc, char* argv[])
{
    std::vector<STR> results;
    std::vector<STR>::const_iterator it;
    int len1, len2;
    char word1[256], word2[256];

    while(fgets(word1, sizeof(word1), stdin) != NULL)
    {
        //trim '\r' or '\n' char;
        len1 = (int)strlen(word1);
        if(len1 && (word1[len1 - 1] == '\r' || word1[len1 - 1] == '\n'))
        {
            word1[len1 - 1] = 0;
            len1--;
        }

        word2[0] = 0;
        fgets(word2, sizeof(word2), stdin);

        //trim '\r' or '\n' char;
        len2 = (int)strlen(word2);
        if(len2 && (word2[len2 - 1] == '\r' || word2[len2 - 1] == '\n'))
        {
            word2[len2 - 1] = 0;
            len2--;
        }

        results.clear();
        if(len1 == len2)
        {
            Convert(word1, word2, len1, results);
            std::sort(results.begin(), results.end(), ASC);
        }

        //output
        printf("[\n");
        for(it = results.begin(); it != results.end(); ++it)
        {
            const char *p1 = it->x;
            while(*p1)
            {
                printf("%c ", *p1);
                ++p1;
            }
            printf("\n");
        }
        printf("]\n");
    }
    return 0;
}


//User-defined predicate function object that defines the comparison
//criterion to be satisfied by successive elements in the ordering. 
//A binary predicate takes two arguments and returns true when satisfied
//and false when not satisfied.

bool ASC(STR s1, STR s2)
{
   return strcmp(s1.x, s2.x) <= 0;
}

bool Convert(const char* word1, const char* word2, int len, std::vector<STR> &results)
{
    std::vector<STR> child1; //子问题 (1) 的解。
    std::vector<STR> child2; //子问题 (2) 的解。
    std::vector<STR>::const_iterator it1, it2;
    STR item;
    int i;

    if(len == 0)
    {
        //放入一个空字符串。
        item.x[0] = 0;
        results.push_back(item);
        return true;
    }
    else if(len == 1)
    {
        if(word1[0] == word2[0])
        {
            strcpy(item.x, "io");
            results.push_back(item);
        }
        return results.size() != 0;
    }

    //when len > 1;
    for(i = 0; i < len; i++)
    {
        // index:  0      i     len-1
        //         |      |      |
        //        [a][x1...][x2...]
        //        [y1...][a][y2...]
        // result: [i  child1  o] * [child2];

        if(word1[0] == word2[i])
        {
            child1.clear();
            Convert(word1 + 1, word2, i, child1);
            if(child1.empty())
                continue;
            
            child2.clear();
            Convert(word1 + i + 1, word2 + i + 1, len - i - 1, child2);
            if(child2.empty())
                continue;

            //解: [i child1 o] * [child2]
            for(it1 = child1.begin(); it1 != child1.end(); ++it1)
            {
                for(it2 = child2.begin(); it2 != child2.end(); ++it2)
                {
                    sprintf(item.x, "i%so%s", it1->x, it2->x);
                    results.push_back(item);
                }
            }
        }
    }
    return results.size() != 0;
}

solution code

  • 总结:

很遗憾的就是,由于时隔多年,ZOJ 网站也早就移交给其他网站托管,新的网址不知何故,我没法提交代码(我的帐号没有了答题权限),所以不知道上面的代码能否 AC。不过对于所有题目给出的示范输入,程序输出都是正确的。希望我已经解决了这个困扰我十多年的一道题目。把代码整理一下以后,最后代码是非常简短的,只有很少的行数,没想到困扰我十多年的问题,就在那天晚上突然冒出来再尝试做一次的想法,只尝试一个晚上就大概做出来了,而且最后的代码量是很少的。最终,是使用递归函数来解决问题,方法并不困难,难点在于,找到把原问题分解成同一类子问题的切入点,使用动态规划算法的问题也是如此。

对于解决具有同类子问题的问题,我们可以想到使用递归函数,递归函数是一个强大的工具,可以解决这种具有同类子问题的问题,但是递归函数也有缺点:如果递归函数的递推深度太深,对线程栈会有压力,会有 ”爆栈“ (Stack Overflow)的风险。要降低这种风险,可以考虑:

(1)减少递推深度(但是这个是由问题本身决定的,所以通常我们很难控制递推深度)。

(2)减小递归函数的栈上空间需求,例如,可以把一些较大的临时对象(例如数组),改为在堆上分配,就可以减小每一层递归函数对栈空间的需求。

(3)改写算法,借助用户自定义的栈,把递归函数改写为非递归函数,相当于把记录和存储函数执行上下文的功能,从线程栈,转交给用户自定义的栈,而用户自定义栈的容量是可以很大的,这样就可以不受线程栈的空间限制。