Codeforces Round #594 (Div. 2)
A - Integer Points
题意:给n条斜率为1和m条斜率为-1的直线,求交点在整点的个数。
题解:观察下面给的图,发现截距相差为偶数的点才有交点在整点。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int t;
scanf("%d", &t);
while(t--) {
int n;
scanf("%d", &n);
int p[2] = {}, q[2] = {};
while(n--) {
int pi;
scanf("%d", &pi);
++p[pi & 1];
}
int m;
scanf("%d", &m);
while(m--) {
int qi;
scanf("%d", &qi);
++q[qi & 1];
}
printf("%lld\n", 1ll * p[0]*q[0] + 1ll * p[1]*q[1]);
}
}
B - Grow The Tree
题意:有n根木棍,要从原点开始横竖横竖间隔摆,求离原点最远距离。
题解:很显然不要求间隔摆就直接一根直接插出去,所以可以试试贪心,用最短的一半下整做竖,其他做横。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[100005];
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
sort(a + 1, a + 1 + n);
ll sum1 = 0;
for(int i = 1, c = n / 2; i <= c; ++i)
sum1 += a[i];
ll sum2 = 0;
for(int i = n / 2 + 1; i <= n; ++i)
sum2 += a[i];
printf("%lld\n", sum1 * sum1 + sum2 * sum2);
}
C - Ivan the Fool and the Probability Theory
题意:给1e5*1e5级别的棋盘格,称棋盘格“随机”当且仅当它每个格子至多有一个相邻的格子与他同色。
不会写,数据量略大。
补题题解:首先考虑只有1行的情况,非常简单,随便dp一下。
然后有一个引理:当确定第1行之后,后面的行只能和相邻行完全相同或者完全相反。证明:若结论不成立,至少存在一个位置 \(j\) ,使得 \(a_{2,j}\) 与 \(a_{1,j}\) 相同,而 \(a_{2,j+1}\) 与 \(a_{1,j+1}\) 相反,那么:当 \(a_{1,j}\) 与 \(a_{1,j+1}\) 同色,则 \(a_{1,j}\) 有两个相邻同色;当 \(a_{1,j}\) 与 \(a_{1,j+1}\) 相反,则 \(a_{2,j}\) 有两个相邻同色。且显然的,假如第1行出现了至少1个连续的2格组,那么以后都不能选完全相同,只能选完全相反,对于这种情况只需要确定第一行就可以全部统计完毕。另一种情况是,只有完全间隔的格子可以选择复制一行,且下一次必须选择相反复制,那这种情况压缩到一列去观察,发现和这个问题的1*m版本是一样的(但是不能够乘2,以为原本求出的dp数组就已经是包含两种颜色的)。
所以就是 \((dp[m]-2)+dp[n]\)
启发:不会做的话是不是可以dp出一行或者2行的结果看看规律,然后手画几个小样例看看能不能套上去吧。不过下一次应该是学会这样分析了。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
int dp[100005][2];
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int n, m;
while(~scanf("%d%d", &n, &m)) {
if(n > m)
swap(n, m);
dp[1][0] = 2;
for(int i = 2; i <= m; ++i) {
dp[i][0] = (dp[i - 1][1] + dp[i - 1][0]) % MOD;
dp[i][1] = dp[i - 1][0];
}
printf("%lld\n", ((1ll * dp[m][0] + dp[m][1] - 2ll) % MOD + (1ll * dp[n][0] + dp[n][1]) % MOD) % MOD);
}
}
D1 - The World Is Just a Programming Task (Easy Version)
见下题
D2 - The World Is Just a Programming Task (Hard Version)
题意:给一个括号串,可以调换其中的任意两个括号的位置(可以自己换自己),使得新括号串的循环移位构成的合法串最多。多方案任输出其中之一。
题解:画一下发现,合法括号串,必须是从0开始的不越过0分界线的到0结束的一段折线。那么循环移位的数量就相当于触碰0点的数量。因为是循环移位所以不妨找出最早的一个最低点作为0点。假如有两个以上0点,那么改变其中之一不会变得更好,此时把0点前的一个非0点的顺序换一下有可能会导致答案+1。
所以口胡出一个做法:先检查是否左右括号平衡,不平衡直接 0 1 1 ,然后检测是否已经满了(都是两个括号间隔着),是则输出 n/2 1 1 ,否则找到零点的数量,若只有一个,则尝试把零点破坏。然后都尝试一下在连续下坡两次的零点前构造一个新的,比比谁多。
但是很难写。我决定看别人怎么做。
补题题解:发现别人和我的分析是一样的,不过别人直接指出了,把一个左括号和一个右括号互换,会使得其中夹着的所有的点的y均-2。所以问题转化成了,对于某个高度为1的左括号,一直向右扩展到第一个高度为1的右括号,可以尝试把中间遇到的所有y=2的点翻折下来,而在这个右括号之后假如跟着左括号(回到2)则可以继续,否则break掉。可以发现这就是被零点打断的一些区间,最好的做法就是在交换两个零点之间的括号。复杂度O(n)。
具体来说,假如编号为0,1,2,3,...,n号点,其中n号和0号是同一个点且他们都是零点,设相邻的某两个零点x1,x2,若x2-x1>2,则尝试交换x1+2,和x2-1。贡献为[x1,x2]中y=2的点的数量。问题转化成怎么找循环位移的零点。这个就不多说了。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
char *s, t[600005];
int gans, gai, gaj;
void solve1() {
//把2翻转成0
int ans1 = 0, ans2 = 0, ai = 1, aj = 1;
int cnt0 = 0, cnt2 = 0, lastp = -1;
for(int i = 1; i <= n; ++i) {
if(s[i] == '(') {
++cnt0;
if(cnt0 == 1)
lastp = i;
else if(cnt0 == 2)
++cnt2;
} else {
--cnt0;
if(cnt0 == 0)
++ans1;
else if(cnt0 == 1) {
if(cnt2 > ans2) {
ans2 = cnt2;
ai = lastp + 1;
aj = i;
}
lastp = i;
cnt2 = 0;
} else if(cnt0 == 2)
++cnt2;
}
}
gans = ans1 + ans2;
gai = ai, gaj = aj;
}
void solve2() {
//把1翻转为-1
int ans = 0, ai = 1, aj = 1;
int cnt0 = 0, cnt1 = 0, lastp = 0;
for(int i = 1; i <= n; ++i) {
if(s[i] == '(') {
++cnt0;
if(cnt0 == 1)
++cnt1;
} else {
--cnt0;
if(cnt0 == 0) {
if(cnt1 > ans) {
ans = cnt1;
ai = lastp + 1;
aj = i;
}
lastp = i;
cnt1 = 0;
} else if(cnt0 == 1)
++cnt1;
}
}
if(ans > gans) {
gans = ans;
gai = ai;
gaj = aj;
}
int p = s - t;
gai += p;
gaj += p;
if(gai > n)
gai -= n;
if(gaj > n)
gaj -= n;
printf("%d\n%d %d\n", gans, gai, gaj);
}
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
while(~scanf("%d%s", &n, t + 1)) {
if(n & 1) {
printf("0\n1 1\n");
continue;
}
int cnt = 0, mincnt = 1e9;
for(int i = 1; i <= n; ++i) {
t[i + n] = t[i];
if(t[i] == '(')
++cnt;
else {
--cnt;
if(cnt < mincnt) {
mincnt = cnt;
s = &t[i];
}
}
}
if(cnt) {
printf("0\n1 1\n");
continue;
}
s[n + 1] = '\0';
solve1();
solve2();
}
return 0;
}
E - Queue in the Train
题意:有n个人坐火车,第i个人会在ti时刻开始想去泡面,每个人花的时间都是p。泡泡面是要排队的,但是大家都不喜欢排队,每个人去排队时,队列中不可以有编号比他小的人(意思是可以有编号比他大的),若多个人同时想去,则神奇的只有编号最小的会去排队。
错误尝试:一开始直接按时间排序然后丢进堆里面按编号弹出来,这个错误理解题意,不过吸收了一下这个对时间扫描的写法(以前貌似不会写,现在码力确实上升了)。应该是直接模拟,对时间排序,然后同一个时刻的按编号顺序排。这样每个人丢进堆里面。这样堆按编号小优先的话,同时想去的这堆人就只能是堆顶了。因为堆顶假如不能去则整个堆都不能去,这是显然的。
所以就再整一个队列。每次查一个位置前面的人是不是有人在队列中的话,用树状数组就可以(求前缀和,貌似从一个二分找奇数偶数前缀和的题目得到启发)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, p;
pair<int, int> t[100005];
priority_queue<int, vector<int>, greater<int> >pq;
queue<int> q;
ll ans[100005];
int bit[100005];
void add(int x, int v) {
for(; x <= n; x += x & -x)
bit[x] += v;
}
int sum(int x) {
int res = 0;
for(; x; x -= x & -x)
res += bit[x];
return res;
}
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
scanf("%d%d", &n, &p);
for(int i = 1; i <= n; ++i) {
scanf("%d", &t[i].first);
t[i].second = i;
}
sort(t + 1, t + 1 + n);
ll curp = 0;
int i = 1, cnt = 0;
while(cnt < n) {
if(q.empty() && pq.empty() && i <= n && curp < t[i].first)
curp = t[i].first;
if(q.size()) {
int u = q.front();
curp += p;
ans[u] = curp;
add(u, -1);
q.pop();
++cnt;
}
while(i <= n && t[i].first <= curp) {
pq.push(t[i].second);
++i;
}
while(pq.size()) {
int u = pq.top();
if(sum(u) == 0) {
q.push(u);
add(u, 1);
pq.pop();
} else
break;
}
}
for(int i = 1; i <= n; ++i)
printf("%lld%c", ans[i], " \n"[i == n]);
return 0;
}
使用这个结构貌似容易颠倒一些事件的顺序,正确的做法是把所有的事件写成结构体然后排序(类似cdq分治),有几种事件:1、有人开始想要泡面,2、有人进入了队伍里排队泡面,3、有人泡完面结束了。
然后发现事件2是可以立刻处理的,就不需要进入优先队列里面了。学习了别人对时间轴的处理方法。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, p;
struct Event {
ll t, op, id;
//时间、事件类型、序号
Event(ll _t, int _op, int _id) {
t = _t, op = _op, id = _id;
}
bool operator<(const Event &e)const {
return t != e.t ? t > e.t : (op != e.op ? op > e.op : id > e.id);
}
};
priority_queue<Event> _time;
set<int> inq, wnt;
ll ans[100005];
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
scanf("%d%d", &n, &p);
for(int i = 1, ti; i <= n; ++i) {
scanf("%d", &ti);
_time.push(Event(ti, 1, i));
}
ll curtime = 0;
while(!_time.empty()) {
Event e = _time.top();
_time.pop();
if(e.op == 1) {
if(inq.empty() || *inq.begin() > e.id) {
curtime = max(curtime, e.t);
curtime += p;
_time.push(Event(curtime, 3, e.id));
inq.insert(e.id);
} else
wnt.insert(e.id);
} else {
ans[e.id] = e.t;
inq.erase(e.id);
if(!wnt.empty() && (inq.empty() || *inq.begin() > *wnt.begin())) {
auto tmp = wnt.begin();
curtime += p;
_time.push(Event(curtime, 3, *tmp));
inq.insert(*tmp);
wnt.erase(tmp);
}
}
}
for(int i = 1; i <= n; ++i)
printf("%lld%c", ans[i], " \n"[i == n]);
return 0;
}
总结1:对这种时间轴,分析得到其实在curtime入队inq之后的下一个有用的1事件必定推迟到curtime+p时发生。而3事件是直接交给时间轴处理的。大概的意思是用优先队列维护一整个时间轴,每次从时间轴的顶部取出一些事件,由于这个题目的约定我们规定事件3早于事件1发生。还是得慢慢领悟。
总结2:时间轴的事件是不随curtime变化的,这个是priority_queue实现的一个优点,curtime的作用只是给事件的发生时间打好标记(就是入队inq了之后,它完成的时间就是curtime+=p。取出一个事件的时候,若curtime超过t,那么t会推迟到curtime执行,否则是curtime浪费时间空等到t,所以取max(然后再+=p)。也就是curtime记录的只是当前队列的队尾元素完成的时间,而不是当前时间,假如换个名字叫做queue_time会更合适。),而不管时间轴是怎么运作的。总之就是码力不足,知道自己的写法为什么错,但是不知道什么是正确的写法。
F - Catowice City
题意:有n个人,选其中j个做裁判,剩下n-j个做参赛选手,要求任意一种合法的分配。合法的分配是指:每个裁判都不认识每个参赛选手。(注意认识关系是单向的,自己必定认识自己)
题解:看起来就像是一个“传递闭包”,认识关系连一条有向边,然后选择一个点必须选择它的整个闭包作为裁判,假如还有点没有选中很明显就可以全部作为参赛选手。问题变成找一个点,不能出发到达所有的点。
补题题解:图论是不太会的,直接看别人的参考。有一种方法是求出点强连通分量,那么一个点强连通分量一定是循环选择的,然后强连通的这个分量缩成一个点。假如缩点之后图中只有一个点,那么就No,否则缩点之后必定无环,选择其中一个出度为0的点作为裁判,剩下的作为选手,因为选手可以认识裁判,裁判不能认识选手。
封装了一个Kosaraju的模板,通过类似网络流的使用方法来添加,实现方式为vector(比前向星略微方便),输出的n2,G2就是缩点后新图的节点数和图的样子,V就是新图每个节点对应的强连通分量,而查询某个点所属的强连通分量则用c2。为了不过多使用::符,在Kosaraju中写一个solve处理新图就可以了。或者using namespace SCC也可以。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
namespace SCC {
const int MAXN = 1e6 + 5;
int n;
vector<int> G[MAXN], BG[MAXN];
int c1[MAXN], cntc1;
int c2[MAXN], cntc2;
int s[MAXN], cnts;
int n2;
vector<int> V[MAXN];
vector<int> G2[MAXN], BG2[MAXN];
void init(int _n) {
n = _n;
cntc1 = 0, cntc2 = 0, cnts = 0;
for(int i = 1; i <= n; ++i) {
G[i].clear();
BG[i].clear();
c1[i] = 0;
c2[i] = 0;
s[i] = 0;
V[i].clear();
G2[i].clear();
BG2[i].clear();
}
}
void add_edge(int u, int v) {
G[u].push_back(v);
BG[v].push_back(u);
}
void dfs1(int u) {
c1[u] = cntc1;
for(int v : G[u]) {
if(!c1[v])
dfs1(v);
}
s[++cnts] = u;
}
void dfs2(int u) {
V[cntc2].push_back(u);
c2[u] = cntc2;
for(int v : BG[u]) {
if(!c2[v])
dfs2(v);
}
}
void Kosaraju() {
for(int i = 1; i <= n; ++i) {
if(!c1[i]) {
++cntc1;
dfs1(i);
}
}
for(int i = n; i >= 1; --i) {
if(!c2[s[i]]) {
++cntc2;
dfs2(s[i]);
}
}
n2 = cntc2;
for(int i = 1; i <= n2; ++i) {
for(auto u : V[i]) {
for(auto v : G[u]) {
if(c2[v] != i) {
G2[i].push_back(c2[v]);
BG2[c2[v]].push_back(i);
}
}
}
}
for(int i = 1; i <= n2; ++i) {
sort(G2[i].begin(), G2[i].end());
G2[i].erase(unique(G2[i].begin(), G2[i].end()), G2[i].end());
sort(BG2[i].begin(), BG2[i].end());
BG2[i].erase(unique(BG2[i].begin(), BG2[i].end()), BG2[i].end());
}
}
void solve() {
int id = -1;
for(int i = 1; i <= n2; ++i) {
if(G2[i].size() == 0) {
id = i;
break;
}
}
printf("%d %d\n", V[id].size(), n - V[id].size());
for(int i = 0, siz = V[id].size(); i < siz; ++i)
printf("%d%c", V[id][i], " \n"[i == siz - 1]);
int fi = 1;
for(int i = 1; i <= n2; ++i) {
if(i != id) {
for(int j = 0, siz = V[i].size(); j < siz; ++j) {
if(fi) {
printf("%d", V[i][j]);
fi = 0;
} else
printf(" %d", V[i][j]);
}
}
}
printf("\n");
}
}
int main() {
#ifdef KisekiPurin
freopen("KisekiPurin.in", "r", stdin);
#endif // KisekiPurin
int t;
scanf("%d", &t);
while(t--) {
int n, m;
scanf("%d%d", &n, &m);
SCC::init(n);
for(int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
SCC::add_edge(u, v);
}
SCC::Kosaraju();
if(SCC::n2 == 1) {
printf("No\n");
continue;
}
printf("Yes\n");
SCC::solve();
}
return 0;
}
上述算法要跑1000ms,仔细想想是对DAG的多重边排序去重导致的,分析一下发现其实假如是DAG上面dp的话多重边其实无所谓的,正向图甚至也不用存。而这道题更绝,只需要找一个出度为0的缩后新点就可以了,连图都不用存。
注:貌似强连通缩点后只有一个顶点的次次都是特殊情况。Codeforces貌似可以有行末空格。
标签:强连通分量
参考资料:
Codeforces 1239B. The World Is Just a Programming Task (Hard Version) - LLTYYC - 博客园
Codeforces 1239D. Catowice City - LLTYYC - 博客园