#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(non_upper_case_globals)]


macro_rules! input {
    (source = $s:expr, $($r:tt)*) => {
        let mut iter = $s.split_whitespace();
        input_inner!{iter, $($r)*}
    };
    ($($r:tt)*) => {
        let mut s = {
            use std::io::Read;
            let mut s = String::new();
            std::io::stdin().read_to_string(&mut s).unwrap();
            s
        };
        let mut iter = s.split_whitespace();
        input_inner!{iter, $($r)*}
    };
}

macro_rules! input_inner {
    ($iter:expr) => {};
    ($iter:expr, ) => {};

    ($iter:expr, $var:ident : $t:tt $($r:tt)*) => {
        let $var = read_value!($iter, $t);
        input_inner!{$iter $($r)*}
    };
}

macro_rules! read_value {
    ($iter:expr, ( $($t:tt),* )) => {
        ( $(read_value!($iter, $t)),* )
    };

    ($iter:expr, [ $t:tt ; $len:expr ]) => {
        (0..$len).map(|_| read_value!($iter, $t)).collect::<Vec<_>>()
    };

    ($next:expr, [$t:tt]) => {
        {
            let len = read_value!($next, usize);
            (0..len).map(|_| read_value!($next, $t)).collect::<Vec<_>>()
        }
    };
    ($iter:expr, chars) => {
        read_value!($iter, String).chars().collect::<Vec<char>>()
    };

    ($iter:expr, usize1) => {
        read_value!($iter, usize) - 1
    };

    ($iter:expr, $t:ty) => {
        $iter.next().unwrap().parse::<$t>().expect("Parse error")
    };
}


#[cfg(debug_assertions)]
macro_rules! mydbg {
    //($arg:expr) => (dbg!($arg))
    ($arg:expr) => (println!("{:?}",$arg));
}


#[cfg(not(debug_assertions))]
macro_rules! mydbg {
    ($($arg:expr),*) => {};
}


use std::cmp;
use std::collections::HashMap;

use std::cell::{RefCell,Cell};
use std::rc::Rc;

pub struct SuffixArray{
    sa : Rc<RefCell<Vec<usize>>>,
    rank : Vec<i32>,
    chars : Vec<char>,
    length : usize,
    tmp : Rc<RefCell<Vec<i32>>>,
    k : Rc<Cell<usize>>,
    lcp_a : Vec<i32>,

}

impl SuffixArray{
    pub fn new(s:String)->Self{
        let n = s.len();
        let c = s.chars().collect::<Vec<char>>();
        let mut rank = vec![0;n+1];
        let mut tmp = Rc::new(RefCell::new(vec![0;n+1]));
        let mut sa :Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(vec![0;n+1]));
        let mut k = Rc::new(Cell::new(1));
        let mut lcp = vec![0;n];


        return SuffixArray{
            sa:sa,
            length:n,
            rank:rank,
            tmp:tmp,
            chars:c,
            k:k,
            lcp_a:lcp,
        };
    }

    pub fn compare_sa2 (&mut self,i: usize,j : usize) -> std::cmp::Ordering{
        let ii = i as usize;
        let jj = j as usize;
        if(self.rank[ii] != self.rank[jj]){
            if(self.rank[ii] < self.rank[jj]){return std::cmp::Ordering::Less}
            else if(self.rank[ii] == self.rank[jj]){return std::cmp::Ordering::Equal}
            else {std::cmp::Ordering::Greater}
        }
        else{
            let i2 = if(ii+self.k.get() >self.length ) { -1} else { self.rank[ii+self.k.get()]};
            let j2 = if(jj+self.k.get() >self.length ) { -1} else { self.rank[jj+self.k.get()]};
            if(i2<j2){return std::cmp::Ordering::Less}
            else if(i2==j2){return std::cmp::Ordering::Equal}
            else {std::cmp::Ordering::Greater}
        }
    }

    #[macro_use]
    pub fn contain(&mut self , target : String)->bool{
        let mut a = 0;
        let mut b = self.length;
        let mut flag = 0;
        let target_c = target.chars().collect::<Vec<char>>();
        let mut index=0;
        while (b-a>=1){
            index+=1;
            if(index>1000){
                break;
            }
            let c = (b+a)/2;
            let sa_c = self.sa.borrow()[c];
            for i in sa_c..(sa_c+target.len()) {                                
                if(self.chars.len() <= i) {
                    flag = -1;
                    break
                };
                if(self.chars[i] != target_c[i-(sa_c as usize)]){
                    flag = self.chars[i] as i32 - target_c[i-(sa_c as usize)] as i32;
                    break;
                }
            }

            if(flag >= 0){
                b=c;
            }
            else{
                a=c;
            }

        }

        
        let sa_b = self.sa.borrow()[b];
        flag = 0;
        for i in sa_b..(sa_b+target.len()) {
            if(self.chars.len() <= i) {
                flag = -1;
                break
            };
            if(self.chars[i] != target_c[i-(sa_b as usize)]){
                flag = self.chars[i] as i32 - target_c[i-(sa_b as usize)] as i32;
                break;
            }
        }
        return if(flag == 0){true} else {false};

    }

    pub fn gen_lcp(&mut self){
        let n  = self.length;
        self.lcp_a[0] = 0;
        for i in 0..n+1{
           self.rank[self.sa.borrow()[i]] = i as i32;
        }

        let mut h = 0;
        for i in 0..n{
            let j = self.sa.borrow()[(self.rank[i]-1) as usize];
            if h>0 {h-=1};
            while(true){
                if(j+h >=n) {break;}
                if(i+h >=n) {break;}
                if self.chars[i+h] != self.chars[j+h] { break;}

                h+=1;
            }

            self.lcp_a[(self.rank[i]-1) as usize]=h as i32;
        }

    }




    pub fn gen(&mut self){
        
        for i in 0..&self.length+1{
            self.sa.borrow_mut()[i] = i;
            self.rank[i] = if i<self.length {self.chars[i] as i32} else {-1};
        }
        
        

        
        while(true){
            if(self.k.get()>self.length) {break};
            
            {
                self.sa.borrow_mut().sort_by(|i,j| -> std::cmp::Ordering{
                let ii = *i as usize;
                let jj = *j as usize;
                if(self.rank[ii] != self.rank[jj]){
                    if(self.rank[ii] < self.rank[jj]){return std::cmp::Ordering::Less}
                    else if(self.rank[ii] == self.rank[jj]){return std::cmp::Ordering::Equal}
                    else {std::cmp::Ordering::Greater}
                }
                else{
                    let i2 = if(ii+self.k.get() >self.length ) { -1} else { self.rank[ii+self.k.get()]};
                    let j2 = if(jj+self.k.get() >self.length ) { -1} else { self.rank[jj+self.k.get()]};
                    if(i2<j2){return std::cmp::Ordering::Less}
                    else if(i2==j2){return std::cmp::Ordering::Equal}
                    else {std::cmp::Ordering::Greater}
                }
            }
);
            }
            {
            self.tmp.borrow_mut()[self.sa.borrow()[0]]=0;
            }
            for i in 1..self.length+1{                
                let a = self.sa.borrow()[i-1];
                let b = self.sa.borrow()[i];
                let fuga = self.compare_sa2(a,b);
                let hoge = if(fuga == std::cmp::Ordering::Greater || fuga == std::cmp::Ordering::Equal)  {0} else {1};
                let c = self.tmp.borrow()[self.sa.borrow()[i-1]];
                self.tmp.borrow_mut()[self.sa.borrow()[i]] = c + hoge;
            }
            for i in 0..self.length+1{
                //self.rank.borrow_mut()[i] = self.tmp.borrow()[i];
                self.rank[i] = self.tmp.borrow()[i];
            }
            self.k.set(self.k.get()*2);
        }




    }
}




pub struct RollingHash{
    hash1:Vec<i64>,
    hash2:Vec<i64>,
    p1:Vec<i64>,
    p2:Vec<i64>,
    b1 : i64,
    b2 : i64,
    mod1 : i64,
    mod2 : i64,
    chars : Vec<char>,
    length : usize,
}

impl RollingHash{
    pub fn new(s:String)->Self{
        let n = s.len();
        let chars = s.chars().collect::<Vec<char>>();
        RollingHash{
            hash1:vec![0;n+1],
            hash2:vec![0;n+1],
            p1:vec![1;n+1],
            p2:vec![1;n+1],
            b1:1234_i64,
            b2:2019_i64,
            mod1:1000000007_i64,
            mod2:1000000009_i64,
            chars : chars,
            length:n,
        }
    }

    pub fn init(&mut self){
        for i in 0..self.length{
            self.hash1[i+1] = (self.hash1[i] * self.b1 + self.chars[i] as i64) % self.mod1;
            self.hash2[i+1] = (self.hash2[i] * self.b2 + self.chars[i] as i64) % self.mod2;
            self.p1[i+1] = (self.p1[i] * self.b1) %self.mod1;
            self.p2[i+1] = (self.p2[i] * self.b2) % self.mod2;
        }
    }

    pub fn get(&mut self,l:usize ,r:usize)->(i64,i64){
        let a = self.hash1[r];
        let b = self.hash1[l]*self.p1[r-l];
        let mut h1:i64 = a - b % self.mod1;
        if h1 < 0 { h1 += self.mod1}
        

        let a = self.hash2[r];
        let b = self.hash2[l]*self.p2[r-l];
        let mut h2:i64 = a - b % self.mod2;
        if h2 < 0 { h2 += self.mod2}
        return (h1,h2);
    }

    pub fn possible(&mut self , m : usize)->bool{
        let mut cache : HashMap<(i64,i64) , usize> = HashMap::new();
        for i in 0..(self.length-m+1){
            let target = self.get(i,i+m);
            if(cache.contains_key(&target)){
                if(cache.get(&target).unwrap() +m  <= i ){
                    return true;
                }
            }
            else {
                cache.insert(target, i);
            }
        }    
        return false;
    }
}


fn main() {

    input! { 
       n: usize,
       s : String,
    }

    let s2 = s.clone();



    let mut rh = RollingHash::new(s2);
    rh.init();
    mydbg!(rh.hash1);
    let mut l=0;
    let mut r = n/2+1;
    while (r - l > 1){
        let mid = (l+r)>>1;
        if(rh.possible(mid)){
            l=mid;
        }
        else {
            r=mid;
        }
    }


    println!("{}",l);



}




Measured memory_bytes: 4352