刷题日志(5)

写在前面的话:我也是初学,有些分析或知识会有错误,望各位大佬们指教

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。然后将 cntm 比较。
    但上述做法有一个小细节没有考虑,所以会 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 的,然后将这个值 cntm 作比较。
    注意,这里的 m 是比中位数大的数的数量,也就是 n * (n-1) / 4,如果 cnt 大于或等于 m,显然 r 太大,否则 l 太小。因为答案算出来的 cntm 大,因此等号放在了取 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 张订单,问是否能满足这些订单,如果不能,输出首个不能满足的订单编号。
  • 分析
    看到区间处理,很容易想到线段树或者树状数组,但是判断订单是否满足时,需要单点查询多次,这两者似乎都不太合适。
    结合树状数组中区间修改的思路,先构建差分数组,然后每个订单修改的实际上只是数组下标为 lr+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 聪明的质监员 (二分 + 前缀和)

原题链接

  • 分析
    本题同样是二分答案,当得出的 Ys 小,向下调整,反之向上调整。关键在于如何快速处理区间内大于 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());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值