ABC 155 F Perils in Parallel
这个题可以抽象成一个串和若干个操作,每个操作能翻转串的子串,问这些操作能否把串翻转成全0
首先想到暴力思路,操作M能让串处于
S
1
S_1
S1和
S
2
S_2
S2两种状态,问题就划分为操作
1
,
2
,
.
.
.
,
M
−
1
1,2,...,M-1
1,2,...,M−1 是否能将
S
1
S_1
S1或者
S
2
S_2
S2的串翻转成全0,继续划分,分支总数为
2
M
2^M
2M,时间复杂度到达指数级肯定是不行的。
接着想想dp方法,
d
p
[
M
]
[
?
]
dp[M][?]
dp[M][?]涉及到M个操作,
?
?
?取什么样的属性呢?想想
d
p
[
M
]
[
?
]
dp[M][?]
dp[M][?]从
d
p
[
M
−
1
]
[
?
]
dp[M-1][?]
dp[M−1][?]中怎么提取信息? 用排除法的话,?不能是串的状态,因为串太长所以不能表示状态,?为空 dp[M]能得到的信息大概是dp[M-1]为true的时候 dp[M]为true,而dp[M-1]不为true的时候,dp[M]能否为true不能判断,感觉dp不行啊
接着想想贪心的方法,M个操作里面有偏序关系存在吗?要说操作之间的区别那就是操作之前存在影响位数大小的差别,差别不能决定偏序关系,而且M个操作不一定都要选,即使有偏序关系也选不了,所以感觉贪心走不远
两大神器都没用,想想是不是有什么特殊的性质,如果一个操作只能翻一个位,那么这个位肯定最后能翻成0,这一位相当没有,如果两个操作翻的位没有交集,那么可以divide and conquer,如果有交集感觉不是很好处理
想到这里,觉得没什么思路,找答案,找到第一名noshi91的答案
#include <algorithm>
#include <functional>
#include <iostream>
#include <map>
#include <set>
#include <utility>
#include <vector>
int main() {
using P = std::pair<int, int>;
int n, m;
std::cin >> n >> m;
/*
問題を言い換えるための下処理をする
*/
std::vector<int> a(n), c(n + 1, 0); // c: 隣接する b の差分
std::vector<P> bombs(n); // first: A, second: B
for (P &e : bombs) {
std::cin >> e.first >> e.second;
}
std::sort(bombs.begin(), bombs.end()); // 座標の昇順にする
for (int i = 0; i < n; i += 1) {
a[i] = bombs[i].first;
c[i] ^= bombs[i].second;
c[i + 1] ^= bombs[i].second;
}
// この処理で c[i] = b[i] xor b[i+1] になる
std::map<P, int> edge_index;
// 辺の両端の pair から辺の index を取得する辞書
// first <= second になっている
// 多重辺があると消えてしまうが、今回は特に困らない
std::vector<std::vector<int>> graph(n + 1); // グラフ
for (int i = 0; i < m; i += 1) {
int l, r;
std::cin >> l >> r;
// l, r を座標圧縮後の値に直す
l = std::lower_bound(a.begin(), a.end(), l) - a.begin();
r = std::upper_bound(a.begin(), a.end(), r) - a.begin();
edge_index[{l, r}] = i;
graph[l].push_back(r);
graph[r].push_back(l);
}
/*
下処理終わり
グラフから辺を選んで両端を反転させ、c を全て 0 にする問題に帰着された
*/
std::set<int> ans; // 使う辺の index の集合
// DFS を行い、辺の使用不使用を決定していく
std::vector<bool> visited(n + 1, false);
std::function<void(int)> dfs = [&](int v) -> void {
visited[v] = true;
for (int u : graph[v]) {
if (visited[u])
continue;
dfs(u);
if (c[u] == 1) {
// uv 辺を使用することに決定
// edge_index[{u, v}] だと u > v の時に正しくアクセスできないことに注意
int id = edge_index[std::minmax(u, v)];
ans.insert(id);
c[u] ^= 1;
c[v] ^= 1;
}
}
};
// グラフが連結とは限らないので、0 から DFS するだけだと足りない
for (int i = 0; i < n + 1; i += 1) {
if (!visited[i])
dfs(i);
// この時点で i は探索済
// それでも c[i] が 0 に出来てないなら、不可能
if (c[i] == 1) {
std::cout << -1 << std::endl;
return 0;
}
}
std::cout << ans.size() << std::endl;
for (int e : ans) {
std::cout << e + 1 << " ";
}
return 0;
}
这个方法是差分,我在算法竞赛书上找到差分这一章0x03,差分这个工具的作用是把原序列上的"区间操作"转化为差分序列上的"单点操作"进行计算 代码里面数组c就是差分数组
b[i] = c[0] xor c[1] xor … xor c[i] 注释是有问题的 应该是c[i] = b[i] xor b[i-1] 且
c
[
0
]
=
=
b
[
0
]
,
c
[
n
]
=
=
b
[
n
−
1
]
c[0] == b[0],c[n] == b[n-1]
c[0]==b[0],c[n]==b[n−1]
第二个for循环是把每个cord的坐标根据a的坐标换成数组a的下标l和r 其中
a
[
l
]
a
[
l
+
1
]
.
.
.
a
[
r
−
1
]
a[l] a[l+1]...a[r-1]
a[l]a[l+1]...a[r−1]是为该cord能翻转的炸弹的坐标,b[l] … b[r-1]都被翻转了,那么
因为 b[l] = c[0] xor c[1] xor … xor c[l],将c[l] flip的话 即 c[l]^=1; 那么b[0]…b[l-1]都不受影响,b[l]…b[n-1]全部都flip,如果将c[r] flip的话 b[r]…b[n-1]都被flip了,所以当c[l]和c[r]同时flip的话,b[l]…b[r-1]都会被flip,而b[1]…b[l-1]和b[r]…b[n-1]都不会flip
将由cord i引起的(l,r)的翻转 存到map中 map[{l,r}] = i,将(l,r)看作是边 存入图graph邻接表中
然后后面进行dfs,要让所有的b翻为0,即需要将所有的c翻为0,这里有个边界 就是
c
[
n
]
c[n]
c[n],因为r可能为n,根据上面说的处理方式 翻转区间
[
l
,
n
)
[l,n)
[l,n)时 c[n]会被flip,所以c[n]存在的意义就在于方便处理边界 即翻转
[
l
,
n
)
[l,n)
[l,n)如果b[0] b[1] … b[n-1]全为0 c[n]应该为0
最后一个核心的点: 为什么dfs是对的
我想了半天没想出来,感觉这个有点玄,首先解决的是这个dfs的工作原理:
我决定把sample input 按这个算法来演算一下:
sample input 1:
b[0]b[1]b[2] 为 101 对应的c[0]c[1]c[2]c[3] 为 1111
边1 为 [0,3) 边2 为 [0,1) 边3 为 [0,0) 边4 为 [1,2)
dfs(0)中 假如先遍历的是1,先dfs(1)
dfs(1)中只能遍历2,先dfs(2)
dfs(2)中所有点邻接点都visited的了,函数里面就只将visited[2] 设为 true 回到dfs(1)
回到dfs(1) 因为c[2]是1 所以将c[1] 和 c[2] (刚从dfs2返回的) 翻转 c[1] c[2] 为 0
此时1所有的邻接边都遍历完了,回到dfs(0)
因为c[1] 为0 所以不进行c[0] 和 c[1] 的翻转 遍历3 dfs(3)
dfs(3)中所有邻接点都visited的了,设置visited[3] 为 true 就返回dfs(0)
因为c[3]是1 所以将c[0] 和 c[3] 翻转 翻转后全部都visited了 最后在for中检查 c中所有值为0
经过这个演算 我发现dfs是如果周围有未访问的点且c值为1的 那么就翻转连接到这个点的边,对于dfs函数 首先是dfs周围未访问的所有点,这样一圈一圈扩散到边界点,最后回到起始的dfs点,这个操作就像水的波纹扩散后遇到边界反射后又收回。
接下来问题是这个dfs一定正确吗,因为dfs每个点,遍历其邻接点的顺序可能会由于邻接表中邻接点的顺序不同而不同。
点u 有邻接点v 和 w 如果先遍历v 之后是返回u后才遍历的w 即除去(u,v)和(u,w)两个边v和w是不连通的,那么很明显 遍历顺序为v w 和 w v没有区别 因为两者不连通,不会影响到分别的连通点
如果除去(u,v)和(u,w)两个边v和w是连通的,因为dfs(x)能保证 x周围所有的边都能变成0(以深度遍历形成的遍历树来看,双亲结点能然其所有子结点变为0,一直向上传递导致根节点以外的所有结点都为0),无论是先dfs(v)还是dfs(w) 出了u以外的全部点都能翻成0,而可能的区别是 翻完以后根节点(u)可能1个为1 一个为0,但是每次翻都是翻两个点 要么 1 1 变 0 0 要么 1 0 变 0 1 可知,无论怎么翻1的个数的奇偶性是不变的,所以先dfs(v)还是先dfs(w) 效果是一样的
有上述可知,遍历其邻接点的顺序的不同不影响dfs的结果,所以一种遍历dfs成功了 其它种遍历dfs一定成功,反之一种失败了 其它都失败,所以这个dfs一定正确
趁热打铁把算法竞赛上面的差分都搞定
CH0304 IncDec Sequence
同上题一样,构造出
c
[
1
]
,
c
[
2
]
,
.
.
.
,
c
[
n
]
c[1], c[2], ... ,c[n]
c[1],c[2],...,c[n] 使得
a
[
i
]
=
c
[
1
]
+
c
[
2
]
+
.
.
.
+
c
[
i
]
a[i] = c[1] + c[2] + ... + c[i]
a[i]=c[1]+c[2]+...+c[i],要最后数都一样 那么就是要使
c
[
2
]
,
c
[
3
]
,
.
.
.
,
c
[
n
]
c[2], c[3], ... ,c[n]
c[2],c[3],...,c[n] 全部为0 边界c[n+1]先默认为0吧
使
[
l
,
r
]
[l,r]
[l,r]全部加1 就是使
c
[
l
]
c[l]
c[l]加1
c
[
r
+
1
]
c[r+1]
c[r+1]减1,反之全部减1 就是使
c
[
l
]
c[l]
c[l]减1
c
[
r
+
1
]
c[r+1]
c[r+1]加1,那么可知一次操作能使一个数加1 能使一个数减1,明显 最少的操作 是先将
c
[
2
]
.
.
.
c
[
n
]
c[2] ... c[n]
c[2]...c[n]的所有正数和所有负数 依次抵消,抵消剩下的 可以用c[1] 或者 c[n+1]与其配对进行抵消,所以最少的次数为 |p - q| + min(p,q) 其中p为
c
[
2
]
,
c
[
3
]
,
.
.
.
,
c
[
n
]
c[2], c[3], ... ,c[n]
c[2],c[3],...,c[n] 中大于0的数的和 q为
c
[
2
]
,
c
[
3
]
,
.
.
.
,
c
[
n
]
c[2], c[3], ... ,c[n]
c[2],c[3],...,c[n] 中小于0的数的和,因为min(p,q)的操作是不涉及到c[1]的,涉及到c[1]的操作可能是0 , 1, 2 ,… ,|p - q|种(因为可能|p-q|操作里面可能涉及c[1] 也可能不涉及c[1]而是涉及c[n+1]) 代码:
max(p,q) == min(p,q) + |p - q|
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
using namespace std;
typedef long long ll;
const int N = 100004;
ll a[N],c[N];
int main()
{
ll n,p = 0,q = 0;//p 是所有正数的绝对值的和,q 是所有负数绝对值的和
scanf("%lld",&n);
for(ll i=1;i<=n;i++){
scanf("%lld",a+i);
c[i] = a[i] - a[i-1];
if(i > 1 && c[i] > 0) p += c[i];
if(i > 1 && c[i] < 0) q -= c[i];
}
printf("%lld\n%lld\n",max(p,q),max(p,q) - min(p,q) + 1);
return 0;
}
POJ 3263 Tallest Cow
说实话这个题如果不是放在这个差分系列里面,我看到这个题可能不会往差分那里想,因为他没有差分的明显标志即对一个区间进行操作
我对这个题 感觉最后题目要求的输出是所有牛的最大可能高度 有两种理解,一种是同时存在的最大高度,另一种理解分别使第i头牛达到最高可能高度,第i牛达到最大可能高度时整体高度可能会不一样
先按简单的理解来 即第一种
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
using namespace std;
const int N = 10004;
int ans[N];//默认一开始所有牛都是等高 且高度都是等于最大牛的高度
int main()
{
int n,i,h,r,a,b;
scanf("%d%d%d%d",&n,&i,&h,&r);
for(int k=1;k<=r;k++){
scanf("%d%d",&a,&b);//a能看到b 就把a和b之间的所有牛的高度降1
ans[min(a,b)+1]--;
ans[max(a,b)]++;
}
for(int k=1;k<=n;k++)
ans[k] += ans[k-1];//将差分数组还原成相对高度数组
for(int k=1;k<=n;k++)
printf("%d\n",ans[k] + h - ans[i]);//每头牛的相对高度差就为 h - ans[i]
return 0;
}
示例通过了 但是是wrong answer,看了一下书上的代码 貌似数据可能出现第三头牛能看到第五头牛,第五头牛看得到第三头牛,这样处理一次就够了,加个map来记录,记录之前的min(a,b),max(a,b)组合是否已经处理过了,改了以后就AC了
AC代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
using namespace std;
const int N = 10004;
int ans[N];
map<pair<int,int>,bool> m;
int main()
{
int n,i,h,r,a,b;
scanf("%d%d%d%d",&n,&i,&h,&r);
for(int k=1;k<=r;k++){
scanf("%d%d",&a,&b);
int u = min(a,b),v = max(a,b);
if(!m[make_pair(u,v)]){
ans[u+1]--;
ans[v]++;
m[make_pair(u,v)] = 1;
}
}
for(int k=1;k<=n;k++)
ans[k] += ans[k-1];
for(int k=1;k<=n;k++)
printf("%d\n",ans[k] + h - ans[i]);
return 0;
}