使用拓扑排序的思想,图解【936.戳印序列】(C++)
Problem: 936. 戳印序列
文章目录
题目:936. 戳印序列
/**
* 拥有一个印章stamp,和一个目标字符串target
* 现在提供一个空白格子,格子长度与target一样
* 每轮我们可以使用印章在格子任意位置改下印章,新轮的印章会覆盖上一轮的字符
* 需要注意的是,每次盖下印章,我们必须保证印章不会出格子界
* 如果我们最后能够通过印章获取target字符,则返回每轮盖下印章,印章最左侧在格子上的位置的数组
* 如:
* stamp = abc , target = ababc 表格 = ?????
* turn one : abc?? [0]
* turn two: ababc [0,2]
* 答案即为 [0,2]
* 如果无法达成目标,则返回空数组
* 限定时间复杂度:10 * target.length
*/
思路
我们先明确一些概念。
记stamp长度为m , target 长度为n。
【窗口】:每次盖下stamp,将覆盖长度为stamp.size()
的字符。我们将每次覆盖的这个长度看做一个窗口。由于本题要求了,盖下印章不能超出边界,所以在目标字符中,存在 n -m + 1个窗口。
【入度】:一般指有向图中,某个顶点入边的数量。如有图v1 -> v2 。那么v2的入度就为1。而在本题中,我们将每个窗口字符视作一个顶点,而其入度为——以该字符为窗首时,与stamp位置不符的字符数量。
【边】:在本题中,当以i位置为窗首字母,窗口长度为m。 i <= j < i + m -1
,当target[j] != stamp[j -i]
时,存在j - > i这样一条边。
【拓扑排序】:拓扑排序是有向无环图顶点特殊的序列,是对DAG(有向无环图)的顶点的一种排序。
- 每个顶点只出现一次
- A在B前,则不存在B->A
AOV(有向无环网)-> 拓扑序列
- 选择无前驱结点(即入度 = 0)的点,out
- 从网中删除该点及其以该点为顶点的有向边(弧尾)
- 重复(1)(2)步骤,直到AOV为空或者网中不存在无前驱的点(即存在环)。
我们每轮out的点,排列出来就是拓扑序列。
而在本题中,我们将获得的拓扑序列逆置,便为结果。
解题方法
描述你的解题方法
通过以上概念,我们现在得到一个有向图。现在来说明解题细节。
1、设置一个windows
数组,其含义为,以windows[i]
为窗首时,该窗口中有多少【入度】(即有多少字符与stamp
对应位置字符不同)。初始化时,假设所有窗口都不符,即windows[i] = m
2、 用一个二维数组存储一个邻接表neighbors
,neighbors[j] = {pos_x....}
(pos_x <= j < pos_x + m),其含义是,当pos_x
作为窗首字符时,target[j] != stamp[j - pos_x]
。也就是说,存在一条 j -> pos_x的边。
2、遍历一遍所有窗口,
-
当遍历某窗口
i
时,存在某个窗口字符与stamp字符不同,则添加一条该位置朝向窗首字符i
的边。否则,windows[i]--
-
如果
windows[i] == 0
,代表这个点的入度为0 ,我们加入队列(即拓扑排序中的out)。
3、因为我们out了该点(记为 i
),可视作target[i, i + m)
中的元素都已匹配正确(归位)。故而我们删除所有以target[i, i+m)
为出度的边,如下图所示(这个图演示的是,我们以1
位置为窗首的节点out)
4、删除所有边后,我们又获得了一些入度=0的节点,将这些节点入队(一旦归位的元素,不要再次去掉其边)。
5、循环3、4、步,如果最后windows
中所有元素值都为0,则代表成功获取拓扑序列,我们将获取的拓扑序列反转,则获得答案。否则,答案不存在,返回空数组。
复杂度
时间复杂度:
添加时间复杂度, 示例: O ( m ( n − m + 1 ) ) O(m(n - m + 1)) O(m(n−m+1))
空间复杂度:
添加空间复杂度, 示例: O ( n ( n − m + 1 ) ) O(n(n-m+1)) O(n(n−m+1))
Code
class Solution
{
public:
vector<int> movesToStamp(string stamp, string target)
{
// 如果长度相等
if(stamp.size() == target.size()){
if(stamp == target){
return {0};
}
return {};
}else if(stamp[0] != target[0]){
return {}; // 因为印章不可以越界,所以第一个字符必须相等
}
int m = stamp.size(), n = target.size();
vector<int> windows(n - m + 1, m);
vector<vector<int>> neighbors(n);
queue<int> nodes;
for(int i = 0; i < windows.size(); i++){
for(int j = 0; j < m; j++){
if(target[i+j] == stamp[j]){
// 代表以i为窗首时,其后有一个字符的位置对应正确
windows[i]--;
if(windows[i] == 0){
// 代表以i为窗首时,target上的窗口字符与stamp完全对应
nodes.emplace(i);
}
}else{
// 代表以i为窗首时, i + j位置上的字符stamp对应位置的字符不相等
neighbors[i + j].emplace_back(i);
}
}
}
vector<int> ans;
// 判断该节点的出度是否删除完
vector<bool> seen(n, false);
while(!nodes.empty()){
// cur窗口已经可以匹配
int cur = nodes.front();
nodes.pop();
ans.push_back(cur);
for(int i= 0; i < m; i++){
if(!seen[cur + i]){
seen[cur + i] = true;
for(auto&& ngb: neighbors[cur + i]){
if(--windows[ngb] == 0){
nodes.emplace(ngb);
}
}
}
}
}
int sum = std::accumulate(windows.begin(), windows.end(),0);
std::reverse(ans.begin(), ans.end());
return sum == 0 ? ans : vector<int>();
}
};