写在前面的话:我也是初学,有些分析或知识会有错误,望各位大佬们指教
目录
- 1:POJ 1733 Parity game(带权并查集 + 离散化)
- 2:POJ 1182 食物链 (带权并查集)
- 3:POJ 2912 Rochambeau (带权并查集 + 枚举)
- 4:POJ 3528 River Hopscotch (二分最大化最小值 + 小细节)
- 5:leetcode 464.我能赢吗 (状压dp)
- 6:leetcode 546.恢复空格 (动态规划 + 字典树)
- 7:POJ 3579 Median (二分 + 边界处理)
- 8:POJ 3865 Matrix (二分 + 二分)
- 9:luogu P1083 借教室 (二分 + 差分)
- 10:luogu P1314 聪明的质监员 (二分 + 前缀和)
1:POJ 1733 Parity game(带权并查集 + 离散化)
- 题意:
有 n 个数,m 条描述,每条描述给出一个区间里1的个数是偶数还是奇数。问从第几条描述开始是错误的。 - 分析:
带权并查集的典型题,对于每一次描述,都判断是否有共同根节点,如果有,则比较给出的描述和实际情况。如果不是在同一张图中,则合并。具体的做法不再详述,有许多博客都讲述了做法。
但本题的数据量非常大,因此需要做离散化,用 map 作映射,保留真正的编号,如果原来没有编号,则分配一个,这样减少了数组空间。 - 代码:
#include <algorithm>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <utility>
#include <vector>
#pragma warning(disable : 4996)
using namespace std;
typedef long long ll;
const int N = 10010;
int n, m, tot, f[N], val[N];
map<int, int> mp;
int insert(int x) {
if (mp.find(x) == mp.end())
mp[x] = tot++;
return mp[x];
}
int find(int x) {
if (x == f[x])
return x;
else
{
int temp = f[x];
f[x] = find(f[x]);
val[x] += val[temp];
return f[x];
}
}
int main() {
int i, x, y, z, res;
char str[10];
scanf("%d", &n);
scanf("%d", &m);
tot = 0;
res = -1;
for (i = 0; i < 2 * m + 1; i++)
f[i] = i;
for (i = 0; i < m; i++)
{
scanf("%d%d%s", &x, &y, str);
if (res != -1)
continue;
if (strcmp(str, "odd") == 0)
z = 1;
else
z = 2;
x--;
x = insert(x);
y = insert(y);
int fx = find(x);
int fy = find(y);
//cout << fx << " " << fy << endl;
if (fx != fy)
{
val[fy] = val[x] + z - val[y];
f[fy] = fx;
}
else
{
int temp = val[y] - val[x];
//cout << temp << " " << z << endl;
if (abs(temp) % 2 != z % 2)
res = i;
}
}
if (res == -1)
res = m;
printf("%d\n", res);
system("pause");
return 0;
}
2:POJ 1182 食物链 (带权并查集)
- 题意:
有一个 A吃B,B吃C,C吃A 的食物链关系,有 n 个动物,是A,B,C中的其中一种,给出 m 条关系,问有多少条是假的。 - 分析:
同样是带权并查集,但是这次关系略有不同,因为三种生物是互相制约的关系。也就是说如果知道 1吃2, 2吃3 ,那就可以知道 3吃1 了,而这个时候有两种情况,第一种是直接推出 3吃1 ,在图中用 (val [ 1 ] - val [ 3 ])% 3 = 1 表示,另一种是从前面所说的关系推出 3吃1,这个时候表达式为 (val [ 3 ] - val [ 1 ] ) % 3 = 2。其他与带权并查集的写法基本一致。 - 代码:
#include <algorithm>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <utility>
#include <vector>
#pragma warning(disable : 4996)
using namespace std;
typedef long long ll;
const int N = 50010;
int f[N], val[N];
int find(int x) {
if (x == f[x])
return x;
else
{
int temp = f[x];
f[x] = find(f[x]);
val[x] += val[temp];
return f[x];
}
}
int main() {
int n, m, i, x, y, z, res;
scanf("%d%d", &n, &m);
for (i = 0; i <= n; i++)
{
f[i] = i;
val[i] = 0;
}
res = 0;
for (i = 0; i < m; i++)
{
scanf("%d%d%d", &z, &x, &y);
if (x > n || y > n || (z == 2 && x == y))
{
res++;
continue;
}
if (z == 1)
{
int fx = find(x);
int fy = find(y);
if (fx == fy)
{
if (abs(val[y] - val[x]) % 3 != 0)
res++;
}
else
{
val[fy] = val[x] + 0 - val[y];
f[fy] = fx;
}
}
else
{
int fx = find(x);
int fy = find(y);
if (fx == fy)
{
if ((val[x] - val[y]) % 3 != 2 && (val[y] - val[x]) % 3 != 1)
res++;
}
else
{
val[fy] = val[x] + 1 - val[y];
f[fy] = fx;
}
}
}
printf("%d\n", res);
system("pause");
return 0;
}
3:POJ 2912 Rochambeau (带权并查集 + 枚举)
- 题意:
有 n 个人进行石头剪刀布游戏,会被分为3组,每一组的人只会出三种中的一个,而他们之中有一个是裁判,这个人可以出任意形式,问给出 m 条描述,能否从这 n 个人中找到裁判,且最快从哪一条语句开始得到结果。 - 分析:
一开始的想法是只有当语句中包含裁判,才会出现矛盾,因此把矛盾语句中两个人的编号都保存起来,然后当中重复出现的就是裁判,如果有多个重复,则输出 Impossible 。但是这样在样例3中,不能找到重复出现的编号,却出现了 Impossible,这时又想是否与备选裁判的数量有关,超过4个时就一定是 Impossible。
显然上述想法难以确保严谨性。应该还是回归最暴力的解法,从 1 - n 枚举裁判,不去处理当前包含编号 i 的语句,如果剩下的依然出现了矛盾,显然该编号不是裁判。如果无矛盾,则将该编号记录下来,出现多个结果,输出 Impossible。
最后如何处理判断出结果的语句位置。我们知道该人是裁判,是建立在其他人的语句中都出现矛盾的基础上,所以应该先判断出其他人不是裁判,于是转换为求其他人情况中矛盾语句出现位置的最大值。
在解题过程中自己还踩了两个坑点,其余解法其实和 题2 食物链 的解法一致。 - 代码:
#include <algorithm>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <utility>
#include <vector>
#pragma warning(disable : 4996)
using namespace std;
typedef long long ll;
const int N = 2010;
int f[N], val[N], first[N], seconde[N];
char ch[N];
int find(int x) {
if (x == f[x])
return x;
else
{
int temp = f[x];
f[x] = find(f[x]);
val[x] += val[temp];
return f[x];
}
}
void init(int n) {
for (int i = 0; i <= n; i++)
{
f[i] = i;
val[i] = 0;
}
}
int main() {
int n, m, i, j, x, y, z, flag, line, cnt;
vector<int> vec;
while (scanf("%d%d", &n, &m) != EOF)
{
for (i = 0; i < m; i++)
scanf("%d %c %d", &first[i], &ch[i], &seconde[i]);
line = 0;
cnt = 0;
vec.clear();
for (i = 0; i < n; i++)
{
init(n);
flag = 1;
for (j = 0; j < m; j++)
{
if (first[j] == i || seconde[j] == i)
continue;
x = first[j];
y = seconde[j];
int fx = find(x);
int fy = find(y);
if (ch[j] == '<')
{
swap(x, y);
swap(fx, fy);
}
if (fx == fy)
{
if (ch[j] == '=' && abs(val[y] - val[x]) % 3 != 0)
{
flag = 0;
line = max(line, j + 1);
break;
}
else if(ch[j] != '=') //一定要判断不是'='
{
if ((val[x] - val[y]) % 3 != 2 && (val[y] - val[x]) % 3 != 1) //不要加abs
{
flag = 0;
line = max(line, j + 1);
break;
}
}
}
else
{
if (ch[j] == '=')
val[fy] = val[x] + 0 - val[y];
else
val[fy] = val[x] + 1 - val[y];
f[fy] = fx;
}
}
if (flag)
{
vec.push_back(i);
cnt++;
}
}
if (cnt == 0)
printf("Impossible\n");
else if (cnt >= 2)
printf("Can not determine\n");
else
printf("Player %d can be determined to be the judge after %d lines\n", vec[0], line);
}
system("pause");
return 0;
}
4:POJ 3528 River Hopscotch (二分最大化最小值 + 小细节)
原题链接](https://vjudge.net/problem/POJ-3258)
- 题意:
有长度为 L 的河,当中有 n 个石子,现在可以从中去除 m 个石子,问去除后路径里的最小值,在所有情况中最大是多少? - 分析:
在去除一个石子后,相应的会有边更改,所以处理起来很麻烦。二分法的一个应用就是枚举可能值,逐步接近正确值,在本题模型中也叫最大化最小值。从 0 - L 开始进行二分, pre 记录上一个石子的位置,遍历到后面的石子时,如果距离小于 mid 说明该石子可以被去除,计数 cnt 加一,否则更新石子位置 pre。然后将 cnt 与 m 比较。
但上述做法有一个小细节没有考虑,所以会 WA。就是当 m == n 时,用二分法得出的结果是 L - 1因此初始范围要改成从 0 - L+1 开始二分。也告诉我们使用二分法时要注意初始范围。 - 代码:
#include <algorithm>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <utility>
#include <vector>
#pragma warning(disable : 4996)
using namespace std;
typedef long long ll;
int f[50010];
int main() {
int L, N, M, i, l, r, mid, pre;
scanf("%d%d%d", &L, &N, &M);
for (i = 1; i <= N; i++)
scanf("%d", &f[i]);
f[0] = 0;
f[N + 1] = L;
sort(f, f + N + 2);
l = 0;
r = f[N + 1] - f[0] + 1;
while (r - l > 1)
{
mid = (l + r) / 2;
int cnt = 0;
pre = f[0];
for (i = 1; i <= N + 1; i++)
{
if (pre + mid > f[i])
cnt++;
else
pre = f[i];
}
if (cnt <= M)
l = mid;
else
r = mid;
}
printf("%d\n", l);
system("pause");
return 0;
}
5:leetcode 464.我能赢吗 (状压dp)
- 原题链接
- 题意:
给定一个目标数 n 和最大选取值 m,两个人从 [ 1,m ] 区间中取非重复的数,谁先到达 n 赢得游戏,问结果。
-分析:
可以归为博弈论的范畴,而博弈论里面的SG函数也有动态规划的影子,而这题可以用递归方式的动态规划做,但重点在于状态的确认。
一开始 WA 了很多次,是因为把状态定为当前取数的总和。但是这个状态会因为取数组合的不同而有所变化。举个例子,n=6,m=3中,dp [ 3 ] 的状态有两种,一种是取1和2,此时先手赢了;第二种是取3,此时先手输了。所以这个状态的划分是不对的。
正解应该是把取数的组合划分为状态,此时可以用状态压缩处理。用位运算和 dfs 搜索来解决该问题。同时,由于可能会出现取数的总和比 n 大的情况,所以要先判断一下。其余的和博弈论基本一致,下一个状态有必败存在,则该状态为必胜,否则本状态为必败。 - 代码:
class Solution {
public static boolean dfs(Boolean[] dp, int state, int maxChoosableInteger, int desiredTotal){
if(dp[state] != null)
return dp[state];
dp[state] = false;
int i;
for(i=1; i <= maxChoosableInteger; i++) //因为要判断是否超出n,所以从1开始
{
if((state & (1 << (i-1))) == 0)
{
if(desiredTotal - i <= 0)
{
dp[state] = true;
break;
}
boolean temp = dfs(dp, state | (1 << (i-1)), maxChoosableInteger, desiredTotal - i);
if(temp == false)
{
dp[state] = true;
break;
}
}
}
return dp[state];
}
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
if(desiredTotal <= maxChoosableInteger)
return true;
if((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal)
return false;
Boolean[] dp = new Boolean[(1 << maxChoosableInteger)];
return dfs(dp, 0, maxChoosableInteger, desiredTotal);
}
}
6:leetcode 546.恢复空格 (动态规划 + 字典树)
- 题意:
给出一个字典,和一个字符串s,在字符串里找到尽可能多的单词,使得剩下的字母个数最少。 - 分析:
本题中需要匹配字符串,所以用字典树较合适,加上动态规划的思想。确定 dp [ i ] 为前 i 个字母里剩余的最少字母,那么初始状态为 dp [ i ] = dp [ i - 1 ] + 1,然后需要与前面的字母作匹配,但是这是后会发现如果进行循环从头开始进行匹配,那么很浪费时间。比较巧妙的方法是在建树时将字典里的单词逆序,这样在匹配过程中可以从 j = i 开始,一直往前匹配。 - 代码:
class Trie{
Trie[] nodes;
boolean isend;
Trie(){
nodes = new Trie[26];
isend = false;
}
public void insert(String s){
Trie now = this;
for(int i=s.length()-1; i>=0; i--){
int num = s.charAt(i) - 'a';
if(now.nodes[num] == null)
now.nodes[num] = new Trie();
now = now.nodes[num];
}
now.isend = true;
}
}
class Solution {
public int respace(String[] dictionary, String sentence) {
int[] dp = new int[sentence.length() + 1];
int n, i, j;
Trie root = new Trie();
Trie now = root;
for (String s : dictionary) {
root.insert(s);
}
n = sentence.length();
for(i=1; i<=n; i++)
{
dp[i] = dp[i-1] + 1;
now = root;
for(j=i; j>0; j--)
{
int num = sentence.charAt(j-1) - 'a';
if(now.nodes[num] == null)
break;
now = now.nodes[num];
if(now.isend)
dp[i] = Math.min(dp[i], dp[j-1]);
}
}
return dp[n];
}
}
7:POJ 3579 Median (二分 + 边界处理)
- 题意:
给出 n 个数,求出其 | Xi - Xj | 组合的中位数。 - 分析:
组合数的总数为 C(n, 2),如果全部都列出来数组会爆。因此要用二分法里 查找第K大值 的思想。先将原有数排序,然后二分逐步逼近,每一次循环用 lower_bound 函数计算在 (a+i, a+n) 的区间里有多少个数是大于 a[ i ] + mid 的,然后将这个值 cnt 与 m 作比较。
注意,这里的 m 是比中位数大的数的数量,也就是 n * (n-1) / 4,如果 cnt 大于或等于 m,显然 r 太大,否则 l 太小。因为答案算出来的 cnt 比 m 大,因此等号放在了取 r 那里。 - 代码:
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<queue>
#include<cmath>
#include<string>
#include<cstdio>
#include<stack>
#include<cstring>
#include<utility>
#include<climits>
#pragma warning(disable : 4996)
using namespace std;
typedef long long ll;
int a[100010];
int main() {
int n, i, l, r, mid, m;
while (scanf("%d", &n) != EOF)
{
for (i = 0; i < n; i++)
scanf("%d", &a[i]);
sort(a, a + n);
m = n * (n - 1) / 4;
l = 0;
r = a[n - 1] - a[0];
while (r - l > 1)
{
mid = (l + r) / 2;
int cnt = 0;
for (i = 0; i < n - 1; i++)
cnt += n - (lower_bound(a + i + 1, a + n, a[i] + mid) - a);
if (cnt <= m)
r = mid;
else
l = mid;
}
printf("%d\n", l);
}
system("pause");
return 0;
}
8:POJ 3865 Matrix (二分 + 二分)
- 题意:
给一个 n * n 的矩阵,给出每一个位置数的计算公式,查找矩阵中第 m 小的数。 - 分析:
依然是二分法中的查找第 k 大的值思想,可以观察到矩阵里的数随着 i 递增,因此每一次二分计算有多少个数是小于 m 的,而计算时又要在每一列中用二分计算有多少个数是比 mid 小的。 - 代码:
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<queue>
#include<cmath>
#include<string>
#include<cstdio>
#include<stack>
#include<cstring>
#include<utility>
#include<climits>
#pragma warning(disable : 4996)
using namespace std;
typedef long long ll;
ll f(ll i, ll j) {
return i * i + 100000 * i + j * j - 100000 * j + i * j;
}
ll solve(ll n, ll m, ll tar) {
ll j, l, r, mid, cnt;
cnt = 0;
for (j = 1; j <= n; j++)
{
l = 0;
r = n + 1;
while (r - l > 1)
{
mid = (l + r) / 2;
if (f(mid, j) < tar)
l = mid;
else
r = mid;
}
cnt += l;
}
return cnt < m;
}
int main() {
ll t, n, m;
ll l, r, mid;
scanf("%lld", &t);
while (t--)
{
scanf("%lld%lld", &n, &m);
l = -1e12;
r = 1e12;
while (r - l > 1)
{
mid = (l + r) / 2;
if (solve(n, m, mid))
l = mid;
else
r = mid;
}
printf("%lld\n", l);
}
system("pause");
return 0;
}
9:luogu P1083 借教室 (二分 + 差分)
- 题意
有 n 天,每天教室数量不同。共有 m 张订单,问是否能满足这些订单,如果不能,输出首个不能满足的订单编号。 - 分析
看到区间处理,很容易想到线段树或者树状数组,但是判断订单是否满足时,需要单点查询多次,这两者似乎都不太合适。
结合树状数组中区间修改的思路,先构建差分数组,然后每个订单修改的实际上只是数组下标为 l 和 r+1 的值。然后结合二分思想,将合法订单数量二分,每次判断 mid 个订单是否会超过教室当天数量即可。 - 代码
#include<bits/stdc++.h>
#pragma warning(disable : 4996)
using namespace std;
typedef long long ll;
const int INF = 0x7f7f7f7f;
const int N = 1000010;
ll day[N], l[N], r[N], d[N], diff[N], need[N];
int n, m;
int solve(int x) {
int i;
memset(diff, 0, sizeof(diff));
for (i = 1; i <= x; i++)
{
diff[l[i]] += d[i];
diff[r[i] + 1] -= d[i];
}
for (i = 1; i <= n; i++)
{
need[i] = need[i - 1] + diff[i];
if (need[i] > day[i])
return 0;
}
return 1;
}
int main() {
int i, dl, dr, mid;
scanf("%d%d", &n, &m);
for (i = 1; i <= n; i++)
scanf("%lld", &day[i]);
for (i = 1; i <= m; i++)
scanf("%lld%lld%lld", &d[i], &l[i], &r[i]);
if (solve(m))
printf("0\n");
else
{
printf("-1\n");
dl = 0;
dr = m;
while (dr - dl > 1)
{
mid = (dl + dr) / 2;
if (solve(mid))
dl = mid;
else
dr = mid;
}
printf("%d\n", dr);
}
system("pause");
return 0;
}
10:luogu P1314 聪明的质监员 (二分 + 前缀和)
- 分析
本题同样是二分答案,当得出的 Y 比 s 小,向下调整,反之向上调整。关键在于如何快速处理区间内大于 W 的 价值v 和其数量。可以采用前缀和处理,每次二分得出 mid 后先预处理前缀和。注意用Java需要BufferedReader快读。 - 代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
class Solution {
private int n, m;
long s;
private long[] w, v;
private int[] dl, dr;
public Solution(int n, int m, long s, long[] w, long[] v, int[] dl, int[] dr) {
this.n = n;
this.m = m;
this.s = s;
this.w = w;
this.v = v;
this.dl = dl;
this.dr = dr;
}
public long check(long W) {
int i;
long[] prew, prev;
prew = new long[200010];
prev = new long[200010];
for (i = 1; i <= n; i++) {
if (w[i] >= W) {
prew[i] = prew[i - 1] + 1;
prev[i] = prev[i - 1] + v[i];
} else {
prew[i] = prew[i - 1];
prev[i] = prev[i - 1];
}
}
long sum = 0;
int l, r;
for (i = 1; i <= m; i++) {
l = dl[i];
r = dr[i];
//System.out.println((prew[r] - prew[l - 1]) * (prev[r] - prev[l - 1]));
sum += (prew[r] - prew[l - 1]) * (prev[r] - prev[l - 1]);
}
return sum;
}
public long solve() {
long l, r, mid, ans;
int i;
l = r = w[1];
for (i = 1; i <= n; i++) {
if (w[i] > r)
r = w[i];
if (w[i] < l)
l = w[i];
}
l--;
r += 2;
ans = Long.MAX_VALUE / 2;
while (r - l > 1) {
mid = (l + r) / 2;
long sum = check(mid);
if (ans > Math.abs(sum - s))
ans = Math.abs(sum - s);
if (sum == s) {
ans = 0;
break;
} else if (sum > s) {
l = mid;
} else {
r = mid;
}
}
return ans;
}
}
public class Main {
public static void main(String[] args) throws IOException {
int n, m;
long s;
long[] w, v;
int[] dl, dr;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = br.readLine();
String[] strings = str.split(" ");
//System.out.println(strings[0]);
n = Integer.parseInt(strings[0]);
m = Integer.parseInt(strings[1]);
s = Long.parseLong(strings[2]);
w = new long[200010];
v = new long[200010];
dl = new int[200010];
dr = new int[200010];
for (int i = 1; i <= n; i++) {
str = br.readLine();
strings = str.split(" ");
w[i] = Long.parseLong(strings[0]);
v[i] = Long.parseLong(strings[1]);
//System.out.println(w[i] + " " + v[i]);
}
for (int i = 1; i <= m; i++) {
str = br.readLine();
strings = str.split(" ");
dl[i] = Integer.parseInt(strings[0]);
dr[i] = Integer.parseInt(strings[1]);
}
System.out.println(new Solution(n, m, s, w, v, dl, dr).solve());
}
}