总结
这场的G题,提醒要初入网络流了。
C - Contest of Rope Pulling
题意:
从两个集合中各自选出若干元素,每个元素两个属性w[i]和v[i],组成集合A和集合B,要求集合A的w[i]之和==集合B的w[i]之和且两个集合的v[i]之和最大。
∑
n
+
m
<
=
1
e
4
,
单
组
n
,
m
<
=
1
e
3
,
w
[
i
]
<
=
1
e
3
,
−
1
e
9
<
=
v
[
i
]
<
=
1
e
9
\sum n + m <= 1e4, 单组 n , m <= 1e3, w[i] <= 1e3, -1e9 <= v[i] <= 1e9
∑n+m<=1e4,单组n,m<=1e3,w[i]<=1e3,−1e9<=v[i]<=1e9
思路:
一眼经典01背包。
假设按照正规写法。
f[i][j]:对于单个组,从前i各种选,w[i]值之和为j的集合。
属性:max
可以发现单组
∑
w
[
i
]
<
=
1
e
6
\sum w[i] <= 1e6
∑w[i]<=1e6, 按照正规写法O(n * 1e6)铁T。
第一种写法。O(sum[1] + sum[2] + … sum[n - 1]) 由于数据水,可以飘过去
先从小到大排序,尽量缩小每次的枚举次数。
int sum = 0; //因为是恰好装满的背包
for(int i = 1 ; i <= n ; i ++) //所以每次转移的max为当前sum[i]。每次有效状态最小为v[i]最大为前缀和sum[i]
{
sum += v[i];
for(int j = sum ; j >= v[i]; j --)
{
// f[i][j] = f[i - 1][j];
// if(j - v[i] >= 0)
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
第二种写法。比较神奇。
随机化+背包。
由于
∑
w
[
i
]
<
=
1
e
6
\sum w[i] <= 1e6
∑w[i]<=1e6,但是ans在f[n + m][0]。
造成了许多的空间浪费。
可以把其中一个组的w[i]重新赋值为-w[i]。
再以随机化的方式重排数组。
一定量的可以缩小背包的体积。
然后采取当前时间复杂度内允许的极限体积maxn。
由于存在负值。
[-maxn, 0, maxn]
可以整体右移动maxn.
[0, maxn, 2 * maxn]
那么对于每次所选物品
i
f
(
a
[
i
]
.
f
i
r
s
t
>
0
)
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
]
,
f
[
i
−
1
]
[
j
−
a
[
i
]
.
f
i
r
s
t
]
+
a
[
i
]
.
s
e
c
o
n
d
)
(
j
−
a
[
i
]
.
f
i
r
s
t
>
=
0
)
if(a[i].first > 0) f[i][j] = max(f[i][j], f[i - 1][j - a[i].first] + a[i].second) \;(j - a[i].first >= 0)
if(a[i].first>0)f[i][j]=max(f[i][j],f[i−1][j−a[i].first]+a[i].second)(j−a[i].first>=0)
e
l
s
e
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
]
,
f
[
i
−
1
]
[
j
−
a
[
i
]
.
f
i
r
s
t
]
+
a
[
i
]
.
s
e
c
o
n
d
(
j
−
a
[
i
]
.
f
i
r
s
t
<
=
2
∗
m
a
x
n
)
else f[i][j] = max(f[i][j], f[i - 1][j - a[i].first] + a[i].second \; (j - a[i].first <= 2 * maxn)
elsef[i][j]=max(f[i][j],f[i−1][j−a[i].first]+a[i].second(j−a[i].first<=2∗maxn)
两个转移方程看似一样,实则表示含义不同。注意约束条件。
PII a[maxn];
ll f[maxn];
int main()
{
int T;
scanf("%d", &T);
while(T --)
{
int n, m;
scanf("%d %d", &n, &m);
for(int i = 1 ; i <= n + m; i ++)
{
scanf("%d %lld", &a[i].first, &a[i].second);
if(i > n)
a[i].first = -a[i].first;
}
random_shuffle(a + 1, a + 1 + n + m);
for(int j = 0; j <= maxn ; j ++)
f[j] = -1e18;
f[100000 / 2] = 0;
for(int i = 1 ; i <= n + m ; i ++)
{
if(a[i].first > 0)
{
for(int j = 100000 ; j >= a[i].first ; j --)
{
f[j] = f[j];
if(f[j - a[i].first] != -1e18) // 大 <---- 小
f[j] = max(f[j], f[j - a[i].first] + a[i].second);
}
}
else
{
for(int j = 0 ; j <= 100000 + a[i].first ; j ++)
{
f[j] = f[j];
if(f[j - a[i].first] != -1e18)
f[j] = max(f[j], f[j - a[i].first] + a[i].second);
}
}
}
printf("%lld\n", f[100000 / 2]);
}
return 0;
}
D - Deliver the Cake
题意:
起点为s,终点为t, 且保证s-- > t连通。
给定n个点及每个点的状态(L, R, M), L:在该点只能左手拿东西, R:在该点只能右手拿,M:在该点左手右手都可以。
求从s–>t的最少时间。每次换手可以任意位置花费x个时间。
1
<
=
n
<
=
1
e
5
,
1
<
=
m
<
=
2
e
5
,
1
<
=
x
<
=
1
e
9
1 <= n <= 1e5, 1 <= m <= 2e5, 1 <= x <= 1e9
1<=n<=1e5,1<=m<=2e5,1<=x<=1e9
∑
n
<
=
2
e
5
,
∑
m
<
=
4
e
5
\sum n<= 2e5, \sum m <= 4e5
∑n<=2e5,∑m<=4e5
思路:
最开始思路假了,认为是最简单的最短路问题。
wa了一发之后,想明白了。
之前写法,能不换手就不换手,每次到达一个新点,如若要换手,就改成当前点的状态,并且加上x,否则不换手,也改成当前点的状态。
题中顶点e,若属性为M,那么在当前点可以选择换手,也可以选择不换手。 会造成两种情况。
- 从L属性的点走到当前点。且当前不换手,那么下次遇到R属性,必须要换手(代码写法,直接把到达M点,且手的属性改成了M,造成下次走R属性的点, 没有换手,造成答案错误)。
- 从R属性的点走到当前点。同理。
赛时虽然想到了DP,但是认为无向图不存在拓扑结构。
竟然想到了把M属性的点拆成L属性和R属性两个点,然后重新跑最短路!!
code:
#include <iostream>
#include <map>
#include <algorithm>
#include <queue>
using namespace std;
typedef long long ll;
typedef pair <ll, int> PII;
const int maxn = 3e5 + 10;
int n, m, s, t;
ll x;
string str[maxn];
int h[maxn], ne[maxn * 6], e[maxn * 6], idx;
ll w[maxn * 6], dis[maxn * 6];
bool book[maxn];
struct note{
int L;
int R;
}A[maxn];
void add(int u, int v, ll ww)
{
w[idx] = ww;
e[idx] = v;
ne[idx] = h[u];
h[u] = idx ++;
}
void djs(int sta)
{
for(int i = 1 ; i <= 3 * n ; i ++)
dis[i] = 1e18, book[i] = false;
dis[sta] = 0;
priority_queue <PII, vector<PII>, greater<PII>> heap;
heap.push({0, sta});
while(heap.size())
{
PII t = heap.top();
heap.pop();
ll a = t.first;
int b = t.second;
if(book[b]) continue;
book[b] = true;
for(int i = h[b] ;i != -1 ; i = ne[i])
{
int j = e[i];
if(str[j] == str[b])
{
if(dis[j] > dis[b] + w[i])
{
dis[j] = dis[b] + w[i];
heap.push({dis[j], j});
}
}
else
{
if(dis[j] > dis[b] + w[i] + x)
{
dis[j] = dis[b] + w[i] + x;
heap.push({dis[j], j});
}
}
}
}
}
int main()
{
int T;
scanf("%d", &T);
while(T --)
{
scanf("%d %d %d %d %lld", &n, &m, &s, &t, &x);
string c;
cin >> c;
int len = c.length();
for(int i = 0 ; i < len ; i ++)
str[i + 1] = c[i];
for(int i = 1 ; i <= 3 * n ; i ++) h[i] = -1, A[i].L = A[i].R = 0;
idx = 0;
int num = n;
for(int i = 1 ; i <= m ; i ++)
{
int u, v;
ll w;
scanf("%d %d %lld", &u, &v, &w);
if(str[u] == "M" && !A[u].L)
{
num ++;
A[u].L = num;
str[num] = "L";
num ++;
A[u].R = num;
str[num] = "R";
}
if(str[v] == "M" && !A[v].L)
{
num ++;
A[v].L = num;// n + 3
str[num] = "L";
num ++;
A[v].R = num; // n + 4
str[num] = "R";
}
if(str[u] != "M" && str[v] != "M") //都不等于
add(u, v, w), add(v, u, w);
else if(str[u] == str[v] && str[u] == "M")
{
add(A[u].L, A[v].L, w), add(A[v].L, A[u].L, w);
add(A[u].L, A[v].R, w), add(A[v].R, A[u].L, w);
add(A[u].R, A[v].L, w), add(A[v].L, A[u].R, w);
add(A[u].R, A[v].R, w), add(A[v].R, A[u].R, w);
}
else if(str[u] == "M" && str[v] != "M")
{
add(A[u].L, v, w), add(v, A[u].L, w);
add(A[u].R, v, w), add(v, A[u].R, w);
}
else if(str[u] != "M" && str[v] == "M")
{
add(u, A[v].L, w), add(A[v].L, u, w);
add(u, A[v].R, w), add(A[v].R, u, w);
}
}
ll res = 2e18;
if(str[s] == "M")
{
if(str[t] == "M")
{
djs(A[s].L);
res = min({res, dis[A[t].L], dis[A[t].R]});
djs(A[s].R);
res = min({res, dis[A[t].L], dis[A[t].R]});
}
else
{
djs(A[s].L);
res = min({res, dis[t]});
djs(A[s].R);
res = min({res, dis[t]});
}
}
else
{
djs(s);
if(str[t] == "M")
{
res = min({res, dis[A[t].L], dis[A[t].R]});
}
else
{
res = min(res, dis[t]);
}
}
printf("%lld\n",res);
}
return 0;
}
/*
每次换手xs 呆在原地
7 6 1 7 100
LMMLMMR
1 2 10
2 3 10
3 4 10
4 5 10
5 6 10
6 7 10
*/
DP写法
在最短路算法中,djs()堆优化算法,本身在求最短路的过程中就满足了拓扑结构,SPFA()由于一个点进队多次,必须在跑完最短路后再建一颗最短路径树,才能跑DP。
感觉这种类似,最短路 + dp状态机。
对于堆中弹出的点,假设一个状态已经更新过所连的边了,那么直接continue就好了。(类似最短路, 只不过改成了状态而已)
这种写法,代码贼短。!!!
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const int maxn = 2e5 + 10;
int n, m, s, t;
int h[maxn], ne[maxn * 2], e[maxn * 2], idx;
ll x, dis[maxn][3], w[maxn * 2];
char str[maxn];
bool book[maxn][3];
void add(int u, int v, ll ww)
{
w[idx] = ww;
e[idx] = v;
ne[idx] = h[u];
h[u] = idx ++;
}
struct note{
int op;
int id;
ll w;
bool operator < (const note &a) const
{
return w > a.w;
}
};
void djs()
{
for(int i = 1 ; i <= n ; i ++) dis[i][0] = dis[i][1] = 1e18, book[i][0] = book[i][1] = false;
priority_queue <note> heap;
if(str[s] == 'M')
{
dis[s][0] = dis[s][1] = 0;
heap.push({0, s, 0ll});
heap.push({1, s, 0ll});
}
else if(str[s] == 'L')
dis[s][0] = 0, heap.push({0, s, 0ll});
else dis[s][1] = 0, heap.push({1, s, 0ll});
while(heap.size())
{
auto t = heap.top();
heap.pop();
int b = t.id;
if(book[b][t.op]) continue;
book[b][t.op] = true;
for(int i = h[b] ; i != -1 ; i = ne[i])
{
int j = e[i];
if(str[j] == 'L' || str[j] == 'M')
{
ll temp = dis[j][0];
dis[j][0] = min({dis[j][0], dis[b][0] + w[i], dis[b][1] + w[i] + x});
if(dis[j][0] < temp)
{
heap.push({0, j, dis[j][0]});
}
}
if(str[j] == 'R' || str[j] == 'M')
{
ll temp = dis[j][1];
dis[j][1] = min({dis[j][1], dis[b][1] + w[i], dis[b][0] + w[i] + x});
if(dis[j][1] < temp)
heap.push({1, j, dis[j][1]});
}
}
}
}
int main()
{
int T;
scanf("%d", &T);
while(T --)
{
scanf("%d %d %d %d %lld", &n, &m, &s, &t, &x);
scanf("%s", str + 1);
for(int i = 1 ; i <= n ; i ++)
h[i] = -1;
idx = 0;
for(int i = 1 ; i <= m ; i ++)
{
int u, v;
ll w;
scanf("%d %d %lld", &u, &v, &w);
add(u, v, w), add(v, u, w);
}
djs();
if(str[t] == 'M')
{
printf("%lld\n", min(dis[t][0], dis[t][1]));
}
else if(str[t] == 'L')
{
printf("%lld\n", dis[t][0]);
}
else printf("%lld\n", dis[t][1]);
}
return 0;
}
E - Equal Sentences
题意:
给定n个单词,求"almost-equal"的单词的数量。
“almost-equal”:原句中第i个单词和新句中第i个单词的下标不超过1。
∑
n
<
=
2
e
5
,
n
<
=
1
e
5
\sum n <= 2e5, n <= 1e5
∑n<=2e5,n<=1e5
思路:
题意说明一个单词可以和前后相距1的单词交换。(且要求相互交换的单词不相同)
交换关系图示,假设2能和1换,那么3就不能和2换,但是4可以继续和3换…。
只要满足相邻不发生连续两次交换即可。
f[i][0/1]:前i个中,且第i个状态为0\1的所有合法方案的集合
属性:cnt
f
[
i
]
[
1
]
=
f
[
i
−
1
]
[
0
]
i
f
(
s
t
r
[
i
]
!
=
s
t
r
[
i
−
1
]
)
f[i][1] = f[i - 1][0] \; if(str[i] != str[i - 1])
f[i][1]=f[i−1][0]if(str[i]!=str[i−1])
f
[
i
]
[
0
]
=
f
[
i
−
1
]
[
1
]
+
f
[
i
−
1
]
[
0
]
f[i][0] = f[i - 1][1] + f[i - 1][0]
f[i][0]=f[i−1][1]+f[i−1][0]
初始化 f[0][0] = 1
code:
#include <iostream>
#include <map>
#include <algorithm>
using namespace std;
const int maxn = 2e5 + 10;
typedef long long ll;
string str[maxn];
ll f[maxn][4];
const ll mod = 1e9 + 7;
ll get_mod1(ll a, ll b)
{
return (a % mod + (b % mod)) % mod;
}
int main() //O(n) 0(nlogn)
{
int T;
scanf("%d", &T);
while(T --)
{
int n;
scanf("%d", &n);
for(int i = 1 ; i <= n ; i ++)
{
cin >> str[i];
f[i][0] = f[i][1] = 0;
}
f[1][0] = 1;
for(int i = 2 ; i <= n ; i ++)
{
if(str[i] != str[i - 1]) f[i][1] = get_mod1(f[i][1], f[i - 1][0]);
f[i][0] = get_mod1(f[i - 1][0] , f[i - 1][1]);
}
printf("%lld\n", get_mod1(f[n][0], f[n][1]));
}
return 0;
}
G - Go Running
题意:
给定一个无限长的x轴,n个信息。求满足n个信息的最少学生的数量。学生可随意选择跑步开始时间,结束时间 ,地点,方向,且速度 = 1m/s。
信息表示为:t[i] x[i]:至少一个学生在t[i]s在x[i]处跑步。
n
<
=
1
e
5
,
1
<
=
t
[
i
]
,
x
[
i
]
<
=
1
e
9
n <= 1e5, 1 <= t[i], x[i] <= 1e9
n<=1e5,1<=t[i],x[i]<=1e9
∑
n
<
=
5
e
5
\sum n <= 5e5
∑n<=5e5
思路:
可知学生跑步距离x = 1 * t + ▲。
观察样例。
假设画一条x–t的坐标轴。那么对于每个点(x,y)经过该点的直线有两条,且斜率分别为1和-1。
贪心的想,要使得每条线经过的点尽量多,才能使得数量最少。
那么对于图中(1,1)点,属于两条直线。交于x轴a点和b点。
不就相当于a—b,a和b之间有一条直线,且要完成覆盖的点为(1,1),既可以选a号点进行覆盖,也可以选择b号点进行覆盖。
不就相当于二分图中的最小点覆盖问题嘛?
观察题目范围,匈牙利跑最大匹配。 时间复杂度O(n * m)水不过去。
只能最大流跑 O(n sqrt(m))。
但是。 网络流。。还属于盲区。
PII a[maxn];
int n;
map <PII, int> mp;
int h[maxn], ne[maxn * 2], e[maxn * 2], w[maxn * 2], idx;
int S, T, cur[maxn], d[maxn];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
// 找增广路 同时将图变成分层图,方便处理环
int bfs()
{
int hh = 0, tt = 0;
memset(d, -1, sizeof d);
queue <int> alls;
d[S] = 0, alls.push(S), cur[S] = h[S];
while(!alls.empty())
{
int u = alls.front();
alls.pop();
for(int i = h[u]; i != -1; i = ne[i])
{
if(w[i] && d[e[i]] == -1)
{
d[e[i]] = d[u] + 1;
cur[e[i]] = h[e[i]];
if(e[i] == T) return 1;
alls.push(e[i]);
}
}
}
return 0;
}
// 更新残留网络
int dfs(int u, int flow)
{
if(u == T) return flow;
int rest = 0, k;
for(int i = cur[u]; i != -1 ; i = ne[i])
{
cur[u] = i; // 当前弧优化
if(w[i] && d[e[i]] == d[u] + 1)
{
k = dfs(e[i], min(flow - rest, w[i]));
if(!k) d[e[i]] = -1; // 3号优化,将分层图中的当前点变成 -1 相当于打上标记
w[i] -= k, w[i ^ 1] += k, rest += k;
if(rest == flow) return flow; // 2号优化
}
}
return rest;
}
int dinic()
{
int res = 0, flow;
while(bfs()) while(flow = dfs(S, INF)) res += flow;
return res;
}
int main() // O(n) O(nlogn)
{
int t;
scanf("%d", &t);
while(t --)
{
scanf("%d", &n);
vector <ll> Alls;
Alls.push_back(-1e9);
mp.clear();
for(int i = 1 ; i <= n ; i ++)
{
ll t, x;
scanf("%lld %lld", &t, &x);
a[i] = {x + t + 1e9, x - t};
Alls.push_back(x + t + 1e9);
Alls.push_back(x - t);
}
sort(Alls.begin(), Alls.end());
Alls.erase(unique(Alls.begin(), Alls.end()), Alls.end()); // n * 2
S = int(Alls.size()), T = S + 1;
memset(h, -1, sizeof(h));
idx = 0;
for(int i = 1 ; i <= n ; i ++)
{
int temp1 = lower_bound(Alls.begin(), Alls.end(), a[i].first) - Alls.begin();
int temp2 = lower_bound(Alls.begin(), Alls.end(), a[i].second) - Alls.begin();
if(!mp.count({temp1, temp2}))
add(temp1, temp2, 1), add(temp2, temp1, 0), mp[{temp1, temp2}] = 1;
if(!mp.count({S, temp1}))
add(S, temp1, 1), add(temp1, S, 0), mp[{S, temp1}] = 1;
if(!mp.count({temp2, T}))
add(temp2, T, 1), add(T, temp2, 0), mp[{temp2, T}] = 1;
}
printf("%d\n", dinic());
}
return 0;
}