G. Hobbits

题意

二维平面上有 $n$ 个点,第 $i$ 个点的坐标是 $(x_i,y_i)$,其中 $x_i < x_{i+1}, \forall i \in [1, n-1]$,点 $i$ 与点 $(i+1)$ 之间由一条线段链接。

在 $(x_n, y_n + H)$ 位置有一个眼睛,现在从点 $1$ 沿着线段走到点 $n$,求这个眼睛的视野能覆盖这个路程中的多长距离?

img

其中,$2 \leq n \leq 2 \cdot 10^5, H \in [1, 10^4], x_i \in [0, 4 \cdot 10^5], y_i \in [0 \leq 10^4]$。

题解

几何题。这里用的是 kuangbin的板子,并且有 板子相对应的讲解

我们设这个眼睛为 $s$,那么可以知道 $(x_n,y_n)$ 在 $s$ 的正下方,接着视线顺时针旋转,每次碰到一个点就判断这个点是否能被看到。

可以知道,这个点能被看见,当且仅当它前面没有点(或相应的线段)能挡住它,判断是否能挡住只要看顺时针扫描的时候的夹角即可,如果右边有个点对应的角更大的话就看不见了。

如果能看见点 $i$,记录这个点为 $pre$,在看见下一个点 $i$ 时,链接 $(s,pre)$ 并且做延长线,与线段 $(i,i+1)$ 相交即可得到对应路程长度。

img


最后注意特判一下平行的情况,如图:

img

如果 $(i,pre)$ 之间刚好是一条线段(这意味着 $pre=i+1$),并且 $s,pre,i$ 在同一条线上,那么是 $(i,pre)$ 整条可以计入答案的。

这里要注意使用 long long 或者板子里的 parallel() 函数来判断,如果用夹角判断会出现精度问题!

• 最后,注意用 scanf() 读入数据,否则超时。

代码
#include <bits/stdc++.h>
using namespace std;
int sgn(double x){
	if(fabs(x) < eps)return 0;
	if(x < 0)return -1;
	else return 1;
}
//square of a double
inline double sqr(double x){return x*x;}
 
struct Point{
	double x,y;
	Point(){}
	Point(double _x,double _y){
		x = _x;
		y = _y;
	}
	void input(){
		scanf("%lf%lf",&x,&y);
	}
	void output(){
		printf("%.2f %.2f\n",x,y);
	}
	bool operator == (Point b)const{
		return sgn(x-b.x) == 0 && sgn(y-b.y) == 0;
	}
	bool operator < (Point b)const{
		return sgn(x-b.x)== 0?sgn(y-b.y)<0:x<b.x;
	}
	Point operator -(const Point &b)const{
		return Point(x-b.x,y-b.y);
	}
	//叉积
	double operator ^(const Point &b)const{
		return x*b.y - y*b.x;
	}
	//点积
	double operator *(const Point &b)const{
		return x*b.x + y*b.y;
	}
	//返回长度
	double len(){
		return hypot(x,y);//库函数
	}
	//返回长度的平方
	double len2(){
		return x*x + y*y;
	}
	//返回两点的距离
	double distance(Point p){
		return hypot(x-p.x,y-p.y);
	}
	Point operator +(const Point &b)const{
		return Point(x+b.x,y+b.y);
	}
	Point operator *(const double &k)const{
		return Point(x*k,y*k);
	}
	Point operator /(const double &k)const{
		return Point(x/k,y/k);
	}
	//`计算pa  和  pb 的夹角`
	//`就是求这个点看a,b 所成的夹角`
	//`测试 LightOJ1203`
	double rad(Point a,Point b){
		Point p = *this;
		return fabs(atan2( fabs((a-p)^(b-p)),(a-p)*(b-p) ));
	}
	//`化为长度为r的向量`
	Point trunc(double r){
		double l = len();
		if(!sgn(l))return *this;
		r /= l;
		return Point(x*r,y*r);
	}
	//`逆时针旋转90度`
	Point rotleft(){
		return Point(-y,x);
	}
	//`顺时针旋转90度`
	Point rotright(){
		return Point(y,-x);
	}
	//`绕着p点逆时针旋转angle`
	Point rotate(Point p,double angle){
		Point v = (*this) - p;
		double c = cos(angle), s = sin(angle);
		return Point(p.x + v.x*c - v.y*s,p.y + v.x*s + v.y*c);
	}
};
 
struct Line{
	Point s,e;
	Line(){}
	Line(Point _s,Point _e){
		s = _s;
		e = _e;
	}
	bool operator ==(Line v){
		return (s == v.s)&&(e == v.e);
	}
	//`根据一个点和倾斜角angle确定直线,0<=angle<pi`
	Line(Point p,double angle){
		s = p;
		if(sgn(angle-pi/2) == 0){
			e = (s + Point(0,1));
		}
		else{
			e = (s + Point(1,tan(angle)));
		}
	}
	//ax+by+c=0
	Line(double a,double b,double c){
		if(sgn(a) == 0){
			s = Point(0,-c/b);
			e = Point(1,-c/b);
		}
		else if(sgn(b) == 0){
			s = Point(-c/a,0);
			e = Point(-c/a,1);
		}
		else{
			s = Point(0,-c/b);
			e = Point(1,(-c-a)/b);
		}
	}
	void input(){
		s.input();
		e.input();
	}
	void adjust(){
		if(e < s)swap(s,e);
	}
	//求线段长度
	double length(){
		return s.distance(e);
	}
	//`返回直线倾斜角 0<=angle<pi`
	double angle(){
		double k = atan2(e.y-s.y,e.x-s.x);
		if(sgn(k) < 0)k += pi;
		if(sgn(k-pi) == 0)k -= pi;
		return k;
	}
	//`点和直线关系`
	//`1  在左侧`
	//`2  在右侧`
	//`3  在直线上`
	int relation(Point p){
		int c = sgn((p-s)^(e-s));
		if(c < 0)return 1;
		else if(c > 0)return 2;
		else return 3;
	}
	// 点在线段上的判断
	bool pointonseg(Point p){
		return sgn((p-s)^(e-s)) == 0 && sgn((p-s)*(p-e)) <= 0;
	}
	//`两向量平行(对应直线平行或重合)`
	bool parallel(Line v){
		return sgn((e-s)^(v.e-v.s)) == 0;
	}
	//`两线段相交判断`
	//`2 规范相交`
	//`1 非规范相交`
	//`0 不相交`
	int segcrossseg(Line v){
		int d1 = sgn((e-s)^(v.s-s));
		int d2 = sgn((e-s)^(v.e-s));
		int d3 = sgn((v.e-v.s)^(s-v.s));
		int d4 = sgn((v.e-v.s)^(e-v.s));
		if( (d1^d2)==-2 && (d3^d4)==-2 )return 2;
		return (d1==0 && sgn((v.s-s)*(v.s-e))<=0) ||
			(d2==0 && sgn((v.e-s)*(v.e-e))<=0) ||
			(d3==0 && sgn((s-v.s)*(s-v.e))<=0) ||
			(d4==0 && sgn((e-v.s)*(e-v.e))<=0);
	}
	//`直线和线段相交判断`
	//`-*this line   -v seg`
	//`2 规范相交`
	//`1 非规范相交`
	//`0 不相交`
	int linecrossseg(Line v){
		int d1 = sgn((e-s)^(v.s-s));
		int d2 = sgn((e-s)^(v.e-s));
		if((d1^d2)==-2) return 2;
		return (d1==0||d2==0);
	}
	//`两直线关系`
	//`0 平行`
	//`1 重合`
	//`2 相交`
	int linecrossline(Line v){
		if((*this).parallel(v))
			return v.relation(s)==3;
		return 2;
	}
	//`求两直线的交点`
	//`要保证两直线不平行或重合`
	Point crosspoint(Line v){
		double a1 = (v.e-v.s)^(s-v.s);
		double a2 = (v.e-v.s)^(e-v.s);
		return Point((s.x*a2-e.x*a1)/(a2-a1),(s.y*a2-e.y*a1)/(a2-a1));
	}
	//点到直线的距离
	double dispointtoline(Point p){
		return fabs((p-s)^(e-s))/length();
	}
	//点到线段的距离
	double dispointtoseg(Point p){
		if(sgn((p-s)*(e-s))<0 || sgn((p-e)*(s-e))<0)
			return min(p.distance(s),p.distance(e));
		return dispointtoline(p);
	}
	//`返回线段到线段的距离`
	//`前提是两线段不相交,相交距离就是0了`
	double dissegtoseg(Line v){
		return min(min(dispointtoseg(v.s),dispointtoseg(v.e)),min(v.dispointtoseg(s),v.dispointtoseg(e)));
	}
	//`返回点p在直线上的投影`
	Point lineprog(Point p){
		return s + ( ((e-s)*((e-s)*(p-s)))/((e-s).len2()) );
	}
	//`返回点p关于直线的对称点`
	Point symmetrypoint(Point p){
		Point q = lineprog(p);
		return Point(2*q.x-p.x,2*q.y-p.y);
	}
};
 
 
int n;
int H;
Point a[maxn];
double ang[maxn];
int main() {
    scanf("%d%d",&n,&H);
    for (int i = 1; i <= n; i++) {
        a[i].input();
    }
    Point p(a[n].x, a[n].y + H);
    for (int i = 1; i <= n; i++) {
        ang[i] = p.rad(a[n], a[i]);
    }
 
    int pre = n;
    double ans = 0.0;
    for (int i = n-1; i >= 1; i--) {
        Line s(p, a[pre]);  // (s, pre)
        Line t(p, a[i]);
        if (s.parallel(t)) {
            if (pre-i == 1) {
                ans += a[i].distance(a[pre]);
            }
            pre = i;
        }
        else if (ang[i] > ang[pre]) {
            Line l(a[i], a[i+1]);  // (i, i+1)
            Point c = l.crosspoint(s);
            ans += c.distance(a[i]);
            pre = i;
        } 
    }
    printf("%.15lf\n", ans);
}

H. K and Medians

题意

给定一个数组,包含 $n$ 个数字 $1, 2, …, n$。

给定一个奇数 $k$,每次操作中,可以选择array中一个大小为 $k$ 的subsequence(不一定连续),保留median,remove掉其他的数字。

问是否可以通过这样的操作,使得最后留下 $m$ 个数,分别为 $b_1, b_2, … b_m$?

其中,$n \in [3, 2 \times 10^5], 3 \leq k \leq n, 1 \leq m < n, 1 \leq b_1 < b_2 … < b_m \leq n$。

题解

脑洞题。

我们首先给出结论:

设 $k = 2a + 1$,

如果我们只考虑所有要最终被remove掉的数,那么如果存在 $i$,使得 $b_i$ 的左边有 $\geq a$ 个要被remove的数,右边也有 $\geq a$ 个要被remove的数,并且 $(n-m)$ 可以被 $(2a)$ 整除,那么答案为 YES,否则为 NO。

这是充要条件,接下来证明:

• $(n-m)$ 可以被 $(2a)$ 整除 是一个显然的条件,在接下来的证明中直接忽略。


存在 $i$,使得 $b_i$ 的左边有 $\geq a$ 个要被remove的数,右边也有 $\geq a$ 个要被remove的数 $\rightarrow$ 有解。

WLOG 我们设左边的数的数量 $\leq$ 右边。

我们一直移除右边,使得右边剩余数字数量 $< 2a$。

此时,左边 $\geq a$,右边 $< 2a$。

并且我们注意到,之前已经有 $(n-m)$ 可以被 $(2a)$ 整除这个条件了,说明我们需要移除偶数个数字,这说明左边需要移除的数字 + 右边需要移除的数字 总量是一个偶数。

这说明,左右奇偶性 相同,又因为我们每次移除 $2a$ 个数字,并且左右两边相差不超过 $a-1$,这说明一定可以有一种移除方法使得左右平衡。

这意味着一定存在这样的remove方案。

• 注意到,在以上证明中,我们没有讨论每次 remove 选择的中位数是什么,因为这并不重要,只要保证在remove的过程中,不会动到那些需要保留的数字即可。


有解 $\rightarrow$ 存在 $i$,使得 $b_i$ 的左边有 $\geq a$ 个要被remove的数,右边也有 $\geq a$ 个要被remove的数。

假设不存在这样的 $i$,说明每次 remove 时,任何需要保留的数字都不能作为中位数。这意味着每次 remove 操作会选择一个最终需要扔掉的数字作为中位数,所以不可能remove干净。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+5;

int n, k, m, a[maxn];
int pre[maxn], suf[maxn];
bool solve() {
    if ((n - m) % (k-1)) return 0;
    for (int i = 1; i <= n; i++) {
        pre[i] = pre[i-1] + a[i];
    }
    for (int i = n; i >= 1; i--) {
        suf[i] = suf[i+1] + a[i];
    }
    for (int i = 1; i <= n; i++) {
        if (!a[i] && pre[i] >= k/2 && suf[i] >= k/2) return 1;
    }
    return 0;
}
int main() {
    int T; cin >> T;
    while (T--) {
        cin >> n >> k >> m;
        for (int i = 1; i <= n; i++) a[i] = 1, pre[i] = suf[i] = 0;
        for (int i = 1; i <= m; i++) {
            int x; cin >> x;
            a[x] = 0;
        }
        bool res = solve();
        if (res) cout << "YES\n";
        else cout << "NO\n";
    }
}

M. Similar Sets

题意

给定 $n$ 个set,一个set内部所有的正整数互不相同。

求是否存在 $2$ 个 set,使得这两个set中拥有至少 $2$ 个相同元素?如果有,输出这两个 set 的index,否则输出 $-1$。

其中,$n \leq 10^5$,并且所有set内的元素总数量 $\leq 2 \times 10^5$,所有元素 $\in [1, 10^9]$。

题解

分块讨论。将 size 大于 $\sqrt n$ 的看作大set,其余看作小 set。

那么有 $3$ 种情况:

  1. 大大
  2. 大小
  3. 小小

对于前两种情况:考虑到大set最多只有 $\sqrt n$ 个,那么只要对于每一个大的set,都维护一个 vis[] 代表这个set里有哪些元素,vis[x] = 1 代表 x 在这个大set内,然后对比其他所有set,判断其他所有set里是否有两个元素使得 vis[x] = 1 成立两次即可。

复杂度:设总共有 $m$ 个元素,那么复杂度为 $O(m\sqrt n)$。


对于第三种情况:

因为每个set都很小,小于 $\sqrt n$,所以对于每个 set 可以考虑处理出所有 pair $(u,v)$,其中 $u < v$,然后判断这个pair是否在其他的小set里。

但这样复杂度太高了,我们有个更优雅的写法:

从小到大枚举所有可能的 $v$,然后枚举这个 $v$ 所在的所有小set(可以预处理得出),在这些小set中枚举所有可能的比 $v$ 小的 $u$,然后判断这个 $u$ 之前是否也出现在另外一个小set中,并且这个小set还包含了 $v$。在代码中,用 last[u] = v, pos[u] = i 很优雅的实现了。

• 本质上来说,对于所有的 $v$,维护了一个list of 所有的 $u$,然后判断在这个list中是否存在两个相同的元素。

复杂度:$O(m\sqrt n)$。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+5;

int T, n;
vector<int> a[maxn];
int sz[maxn];
vector<int> big, small;

bool done = 0;
bool vis[maxn];
void solve_big() {
    for (int i : big) {
        for (int x : a[i]) vis[x] = 1;
        for (int j = 1; j <= n; j++) {
            if (j == i) continue;
            int cnt = 0;
            for (int x : a[j]) {
                if (vis[x]) cnt++;
            }
            if (cnt >= 2) {
                done = 1;
                printf("%d %d\n", i, j);
                return;
            }
        }
        for (int x : a[i]) vis[x] = 0;
    }
}


int last[maxn], pos[maxn];
set<int> small_num;  // 储存所有 small 里面的数字
vector<int> p[maxn];  // p[v]: 这个值所在的所有 small set 的index
void solve_small() {
    for (int v : small_num) {
        for (int i : p[v]) {
            for (int j = 0; j < a[i].size() && a[i][j] < v; j++) {
                int u = a[i][j];
                if (last[u] == v) {
                    done = 1;
                    printf("%d %d\n", i, pos[u]);
                    return;
                } else {
                    last[u] = v;
                    pos[u] = i;
                }
            }
        }
    }
}

map<int, int> id;
int cnt = 0;
void clearall() {
    for (int i = 1; i <= n; i++) {
        for (int j : a[i]) vis[j] = 0, p[j].clear(), last[j] = pos[j] = 0;
    }
    for (int i = 1; i <= n; i++) {
        sz[i] = 0;
        a[i].clear();
    }
    big.clear();
    small.clear();
    small_num.clear();
    done = 0;
    id.clear();
    cnt = 0;
}

const int M = 400;
int main() {
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%d", &sz[i]);
            for (int j = 1; j <= sz[i]; j++) {
                int x; scanf("%d", &x);
                if (!id.count(x)) {
                    id[x] = ++cnt;
                } 
                a[i].push_back(id[x]);
            }
            sort(a[i].begin(), a[i].end());
            if (sz[i] > M) big.push_back(i);
            else small.push_back(i);
        }
        for (int i : small) {
            for (int j : a[i]) {
                small_num.insert(j), p[j].push_back(i);
            }
        }

        // 先处理 big
        solve_big();
        if (!done) {
            solve_small();
        }
        if (!done) printf("-1\n");

        clearall();
    }
}